From de144079db5a1223d4e612a6c8409cee65b1fb96 Mon Sep 17 00:00:00 2001 From: bplost Date: Tue, 23 Aug 2016 10:32:16 -0500 Subject: [PATCH 1/7] Add support for TLS/SSL mutual authentication Add support in the TLS_FTPHandler to check a client certificate. This type of support strengthens the security between the client and the server, only allowing clients with a valid certificate to connect to the server. Updated the api.rst file with the two new configurable options to make client authentication work --- CREDITS | 9 ++ HISTORY.rst | 4 + docs/api.rst | 8 + pyftpdlib/handlers.py | 27 +++- pyftpdlib/test/clientcert.pem | 50 +++++++ .../functional_ssl_client_certfile_tests.py | 137 ++++++++++++++++++ 6 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 pyftpdlib/test/clientcert.pem create mode 100644 pyftpdlib/test/functional_ssl_client_certfile_tests.py diff --git a/CREDITS b/CREDITS index 34fdfebb..d64e467b 100644 --- a/CREDITS +++ b/CREDITS @@ -178,3 +178,12 @@ N: Dmitry Panov C: UK E: dop251@gmail.com D: issue 262 + +N: Zuo Haocheng +C: CN +E: zuohaocheng1022@gmail.com +D: issue 336 + +N: Brandon Plost +E: bplost@slb.com +D: issue 336 \ No newline at end of file diff --git a/HISTORY.rst b/HISTORY.rst index ad156766..fd15bed5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,10 @@ Bug tracker at https://github.com/giampaolo/pyftpdlib/issues Version: 1.5.3 - XXXX-XX-XX =========================== +**Enhancements** + +- #336: Support client certificate authentication. + **Bug fixes** - #414: Respond successfully to STOR only after closing file handle. diff --git a/docs/api.rst b/docs/api.rst index fa5c9303..e363a6bf 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -556,7 +556,15 @@ Extended handlers When True requires SSL/TLS to be established on the data channel. This means the user will have to issue PROT before PASV or PORT (default ``False``). + + .. data:: client_certfile + The path of the certificate to check the client certificate against. + When provided, only allowing clients with a valid certificate to connect + to the server (default ``None``). + + .. versionadded:: 1.5.3 + Extended authorizers -------------------- diff --git a/pyftpdlib/handlers.py b/pyftpdlib/handlers.py index 385a21e0..cd777dc4 100644 --- a/pyftpdlib/handlers.py +++ b/pyftpdlib/handlers.py @@ -876,7 +876,7 @@ def close(self): # Close file object before responding successfully to client if self.file_obj is not None and not self.file_obj.closed: self.file_obj.close() - + if self._resp: self.cmd_channel.respond(self._resp[0], logfun=self._resp[1]) @@ -3419,6 +3419,8 @@ class TLS_FTPHandler(SSLConnection, FTPHandler): certfile = None keyfile = None ssl_protocol = SSL.SSLv23_METHOD + # client certificate configurable attributes + client_certfile = None # - SSLv2 is easily broken and is considered harmful and dangerous # - SSLv3 has several problems and is now dangerous # - Disable compression to prevent CRIME attacks for OpenSSL 1.0+ @@ -3453,10 +3455,27 @@ def __init__(self, conn, server, ioloop=None): self._pbsz = False self._prot = False self.ssl_context = self.get_ssl_context() + if self.client_certfile is not None: + from OpenSSL.SSL import VERIFY_CLIENT_ONCE + from OpenSSL.SSL import VERIFY_FAIL_IF_NO_PEER_CERT + from OpenSSL.SSL import VERIFY_PEER + self.ssl_context.set_verify(VERIFY_PEER | + VERIFY_FAIL_IF_NO_PEER_CERT | + VERIFY_CLIENT_ONCE, + self.verify_certs_callback) def __repr__(self): return FTPHandler.__repr__(self) + # Cannot be @classmethod, need instance to log + def verify_certs_callback(self, connection, x509, + errnum, errdepth, ok): + if not ok: + self.log("Bad client certificate detected.") + else: + self.log("Client certificate is valid.") + return ok + @classmethod def get_ssl_context(cls): if cls.ssl_context is None: @@ -3471,6 +3490,12 @@ def get_ssl_context(cls): if not cls.keyfile: cls.keyfile = cls.certfile cls.ssl_context.use_privatekey_file(cls.keyfile) + if cls.client_certfile is not None: + from OpenSSL.SSL import OP_NO_TICKET + from OpenSSL.SSL import SESS_CACHE_OFF + cls.ssl_context.load_verify_locations(cls.client_certfile) + cls.ssl_context.set_session_cache_mode(SESS_CACHE_OFF) + cls.ssl_options = cls.ssl_options | OP_NO_TICKET if cls.ssl_options: cls.ssl_context.set_options(cls.ssl_options) return cls.ssl_context diff --git a/pyftpdlib/test/clientcert.pem b/pyftpdlib/test/clientcert.pem new file mode 100644 index 00000000..4c755845 --- /dev/null +++ b/pyftpdlib/test/clientcert.pem @@ -0,0 +1,50 @@ +-----BEGIN CERTIFICATE----- +MIIDqzCCApOgAwIBAgIBADANBgkqhkiG9w0BAQsFADBwMQswCQYDVQQGEwJVUzEO +MAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xDDAKBgNVBAoMA1NMQjEM +MAoGA1UECwwDU1dUMQwwCgYDVQQDDANzd3QxFTATBgkqhkiG9w0BCQEWBmJwbG9z +dDAeFw0xNjA4MjMxMzQyMDZaFw0xNzA4MjMxMzQyMDZaMHAxCzAJBgNVBAYTAlVT +MQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEMMAoGA1UECgwDU0xC +MQwwCgYDVQQLDANTV1QxDDAKBgNVBAMMA3N3dDEVMBMGCSqGSIb3DQEJARYGYnBs +b3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArNnEnnUGpXLqnTTx +3td4OWQoKFBppifL6+r5y839CWT6GR3Xj5OlcJPXMKBO40H0StHOctshPL0/ZZeC +sNHT6O6c47nBc/pw3YX4GJjLJno0k1+xTSvmk+yhTF/i1ThDIq2YrlGWXHyo/jOe +gJc0T3L2u1Ivx+iOEAIP9uqBpAi3rhfZKBvVdDXb5J0TqouXt4jx5l8Fq577D9y4 +W61nmj7FlquxClhGIgsNbMtlBIMkALNLq3kY+TqYatjRy6aS6mb55TfObjP+IOgz +8Jln937hb89eJopirwKMzD1EnJgBamMPNJjIhDQlHklMwkgynIKnSJghbjKBusaE +2xZEPQIDAQABo1AwTjAdBgNVHQ4EFgQU2hMnpneH1sZ79iWoBLue1+7f4NwwHwYD +VR0jBBgwFoAU2hMnpneH1sZ79iWoBLue1+7f4NwwDAYDVR0TBAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAQEAdmPairr/lt63J3dfTSFNJTytvV1WW7Ak8NwH1hdheaYy +Tx9ffjRIZv9WyHEWb/1YCkKo08LqgnVh07HfW0JY1hqD0SwoHtPexTIgBnOsKvCr +q1gQjFuDg2wVV7cecYPQFYv9jweIe62OCapKl8PjmXii+qnxY/Qbbyx9bGYbR1k4 +KJm073WwiqXXCS1JgOj9WH3I1Qa2Ptb6RO+Woy7ItA5ftQSp4EMTwhygOK0j0w9V +11MAPUtZ8/rTiD17HHVzKfbNmx4E6dtBV9E/gn464lrdNhxEaeN2wW42FU+CzeVa +/aYQbQTSXpI5tX7QTAcQrMAqp75EBMdYj5+9bRXIvw== +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCs2cSedQalcuqd +NPHe13g5ZCgoUGmmJ8vr6vnLzf0JZPoZHdePk6Vwk9cwoE7jQfRK0c5y2yE8vT9l +l4Kw0dPo7pzjucFz+nDdhfgYmMsmejSTX7FNK+aT7KFMX+LVOEMirZiuUZZcfKj+ +M56AlzRPcva7Ui/H6I4QAg/26oGkCLeuF9koG9V0NdvknROqi5e3iPHmXwWrnvsP +3LhbrWeaPsWWq7EKWEYiCw1sy2UEgyQAs0ureRj5Ophq2NHLppLqZvnlN85uM/4g +6DPwmWf3fuFvz14mimKvAozMPUScmAFqYw80mMiENCUeSUzCSDKcgqdImCFuMoG6 +xoTbFkQ9AgMBAAECggEBAKRQ3G36N9g+VzQdKbU6xipgwSAZ2WU/vcZG+TI6Xsp4 +eJw51zrBE+viTxYFvxihETezHXvoPj98dHECSBYJUlbDxtdhNbsoH/UmrwPK9Ixe +be6PcIA5NJf4whlVqdAiDQhBWLyWCMdhJlGJBqudkffZBR5r8corlCk5nK2Qnq8s +ncaaO1A7RLFXXCiwWdAi7bJEHZB2XFQYMctfJ+/m02s0IhIRmusH4XovdTucfykI +FstXtd5+AOfqsOkTn4kGOmG4ObKOXOp1XAjTTIV3xxU0CzYxE1CPhlimkAVLuRPs +bInojdtm80ZQY68xCOvUsq+WMIB+dKkeJKeTI6e/b+0CgYEA3nDQFto7pqtVZLUu +YVTlR6vxrXEYl8FIes8WlICBWXPnviFoiSLTB5SPF1Yvv/LKFkYQ96i2ep8FvZEy +Rbzt1xOOF9GWtO4lc6cBIzh31POJgY2B2PAo7qmhpyhutCaaotf5ukpx35eGlj5v +RT0wDN7Xt6S4arqgbngUdtHLhqsCgYEAxu2rv/GVU/DL3VuBHIky/OKFeL94EnIA +UUm5SRvd0SeHgcjd+EHG/u1nH1XC+Dwiw75Dsv/nqhekFqaQczYZlUrKa+mkdofV +i8DbXHies6qTwvYUIorhaiAoY+iRwJ/6d20oep+3fDSPPcVj7PakZpCK0sAesWX7 +09muN0vLALcCgYAKr0iPkHQFEX3MlJdhvX417yBwwFn6ECK3I3NmNrX/4f1juJ8Y +1z9jwdMNv+oTQkpKv5rZCpWZVkIkVPEhQG38Qsg0hLDEiBvsbj0zv+ahqAEW5AE0 +tnSA4k0Nhneq15/d6pnoROMrZk/kr6MQpFvGgn3CKHtjRQunwsTY4ELyeQKBgQCN +zlNGqvJmOhs5msc5Dly4hMncv7DahUXQrJtWkHTZajJgxE3ncQxoIdgHMF2iE0w8 ++V7NNTtxtxSTyPzkBEbMc9pEfvNsQ3xo+XvmOV34ebqHml/UF+iEfJQOVHXCOMiV +Zc0bTMvB0L3jrNiEzXV4X8V2Ytn+X9LavCxC4ta9lQKBgHTQNDz+qdiLoeX68HKp +b5jd1H7nozWdcbcAO5tKNbSZvnY05QCjC+WkuIeUkKc2zcctIy306/iFRApguElm +z8sm8NnJIkJeRx2XmEE5Lcn64se3ml7qVlcFaYrVW8hDrrvlUAwzu+ZoPVD3DSiQ +EOnPOa2mEwR9YPyPaKdq+fkt +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/pyftpdlib/test/functional_ssl_client_certfile_tests.py b/pyftpdlib/test/functional_ssl_client_certfile_tests.py new file mode 100644 index 00000000..abffb5a7 --- /dev/null +++ b/pyftpdlib/test/functional_ssl_client_certfile_tests.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python + +# Copyright (C) 2007-2016 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. +# +# +# Does not follow naming convention of other tests because this +# CANNOT be run in the same test suite with test_functional_ssl. +# The test parallelism causes SSL errors when there should be none +# Please run these tests separately + +import ftplib +import os +import sys + +import OpenSSL # requires "pip install pyopenssl" + +from pyftpdlib.handlers import TLS_FTPHandler +from pyftpdlib.test import configure_logging +from pyftpdlib.test import remove_test_files +from pyftpdlib.test import ThreadedTestFTPd +from pyftpdlib.test import TIMEOUT +from pyftpdlib.test import unittest +from pyftpdlib.test import VERBOSITY +from _ssl import SSLError + + +FTPS_SUPPORT = hasattr(ftplib, 'FTP_TLS') +if sys.version_info < (2, 7): + FTPS_UNSUPPORT_REASON = "requires python 2.7+" +else: + FTPS_UNSUPPORT_REASON = "FTPS test skipped" + +CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), + 'keycert.pem')) +CLIENT_CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), + 'clientcert.pem')) + +del OpenSSL + + +if FTPS_SUPPORT: + class FTPSClient(ftplib.FTP_TLS): + """A modified version of ftplib.FTP_TLS class which implicitly + secure the data connection after login(). + """ + + def login(self, *args, **kwargs): + ftplib.FTP_TLS.login(self, *args, **kwargs) + self.prot_p() + + class FTPSServerAuth(ThreadedTestFTPd): + """A threaded FTPS server that forces client certificate + authentication used for functional testing. + """ + handler = TLS_FTPHandler + handler.certfile = CERTFILE + handler.client_certfile = CLIENT_CERTFILE + + +# ===================================================================== +# dedicated FTPS tests with client authentication +# ===================================================================== + + +@unittest.skipUnless(FTPS_SUPPORT, FTPS_UNSUPPORT_REASON) +class TestFTPS(unittest.TestCase): + """Specific tests for TLS_FTPHandler class.""" + + def setUp(self): + self.server = FTPSServerAuth() + self.server.start() + + def tearDown(self): + self.client.close() + self.server.stop() + + def assertRaisesWithMsg(self, excClass, msg, callableObj, *args, **kwargs): + try: + callableObj(*args, **kwargs) + except excClass as err: + if str(err) == msg: + return + raise self.failureException("%s != %s" % (str(err), msg)) + else: + if hasattr(excClass, '__name__'): + excName = excClass.__name__ + else: + excName = str(excClass) + raise self.failureException("%s not raised" % excName) + + def test_auth_client_cert(self): + self.client = ftplib.FTP_TLS(timeout=TIMEOUT, certfile=CLIENT_CERTFILE) + self.client.connect(self.server.host, self.server.port) + # secured + try: + self.client.login() + except Exception: + self.fail("login with certificate should work") + + def test_auth_client_nocert(self): + self.client = ftplib.FTP_TLS(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + try: + self.client.login() + except SSLError as e: + # client should not be able to log in + if "SSLV3_ALERT_HANDSHAKE_FAILURE" in e.reason: + pass + else: + self.fail("Incorrect SSL error with" + + " missing client certificate") + else: + self.fail("Client able to log in with no certificate") + + def test_auth_client_badcert(self): + self.client = ftplib.FTP_TLS(timeout=TIMEOUT, certfile=CERTFILE) + self.client.connect(self.server.host, self.server.port) + try: + self.client.login() + except Exception as e: + # client should not be able to log in + if "TLSV1_ALERT_UNKNOWN_CA" in e.reason: + pass + else: + self.fail("Incorrect SSL error with bad client certificate") + else: + self.fail("Client able to log in with bad certificate") + + +configure_logging() +remove_test_files() + + +if __name__ == '__main__': + unittest.main(verbosity=VERBOSITY) From c224708056aff42223e6b9f65ae25cbb972acac7 Mon Sep 17 00:00:00 2001 From: Zuo Haocheng Date: Mon, 17 Jul 2017 13:33:10 +0800 Subject: [PATCH 2/7] Use ssl.context for ftp client in ssl tests --- .../functional_ssl_client_certfile_tests.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/pyftpdlib/test/functional_ssl_client_certfile_tests.py b/pyftpdlib/test/functional_ssl_client_certfile_tests.py index abffb5a7..7c5d93c5 100644 --- a/pyftpdlib/test/functional_ssl_client_certfile_tests.py +++ b/pyftpdlib/test/functional_ssl_client_certfile_tests.py @@ -24,6 +24,7 @@ from pyftpdlib.test import unittest from pyftpdlib.test import VERBOSITY from _ssl import SSLError +import ssl FTPS_SUPPORT = hasattr(ftplib, 'FTP_TLS') @@ -73,6 +74,11 @@ def setUp(self): self.server.start() def tearDown(self): + self.client.ssl_version = ssl.PROTOCOL_SSLv23 + with self.server.lock: + self.server.handler.ssl_version = ssl.PROTOCOL_SSLv23 + self.server.handler.tls_control_required = False + self.server.handler.tls_data_required = False self.client.close() self.server.stop() @@ -90,8 +96,18 @@ def assertRaisesWithMsg(self, excClass, msg, callableObj, *args, **kwargs): excName = str(excClass) raise self.failureException("%s not raised" % excName) + @classmethod + def get_ssl_context(cls, certfile): + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + if certfile: + ssl_context.load_cert_chain(certfile) + return ssl_context + def test_auth_client_cert(self): - self.client = ftplib.FTP_TLS(timeout=TIMEOUT, certfile=CLIENT_CERTFILE) + ctx = self.get_ssl_context(CLIENT_CERTFILE) + self.client = ftplib.FTP_TLS(timeout=TIMEOUT, context=ctx) self.client.connect(self.server.host, self.server.port) # secured try: @@ -115,7 +131,8 @@ def test_auth_client_nocert(self): self.fail("Client able to log in with no certificate") def test_auth_client_badcert(self): - self.client = ftplib.FTP_TLS(timeout=TIMEOUT, certfile=CERTFILE) + ctx = self.get_ssl_context(CERTFILE) + self.client = ftplib.FTP_TLS(timeout=TIMEOUT, context=ctx) self.client.connect(self.server.host, self.server.port) try: self.client.login() From 3ab87234bdfedfa473706ea31c8ab87e41e6fcf0 Mon Sep 17 00:00:00 2001 From: Zuo Haocheng Date: Mon, 17 Jul 2017 15:03:22 +0800 Subject: [PATCH 3/7] Make handlers.get_ssl_context non-static to support multi-threaded environment --- pyftpdlib/handlers.py | 52 +++--- pyftpdlib/servers.py | 4 +- .../functional_ssl_client_certfile_tests.py | 154 ------------------ pyftpdlib/test/test_functional.py | 2 +- pyftpdlib/test/test_functional_ssl.py | 92 +++++++++++ 5 files changed, 120 insertions(+), 184 deletions(-) delete mode 100644 pyftpdlib/test/functional_ssl_client_certfile_tests.py diff --git a/pyftpdlib/handlers.py b/pyftpdlib/handlers.py index cd777dc4..a3883658 100644 --- a/pyftpdlib/handlers.py +++ b/pyftpdlib/handlers.py @@ -3455,19 +3455,10 @@ def __init__(self, conn, server, ioloop=None): self._pbsz = False self._prot = False self.ssl_context = self.get_ssl_context() - if self.client_certfile is not None: - from OpenSSL.SSL import VERIFY_CLIENT_ONCE - from OpenSSL.SSL import VERIFY_FAIL_IF_NO_PEER_CERT - from OpenSSL.SSL import VERIFY_PEER - self.ssl_context.set_verify(VERIFY_PEER | - VERIFY_FAIL_IF_NO_PEER_CERT | - VERIFY_CLIENT_ONCE, - self.verify_certs_callback) def __repr__(self): return FTPHandler.__repr__(self) - # Cannot be @classmethod, need instance to log def verify_certs_callback(self, connection, x509, errnum, errdepth, ok): if not ok: @@ -3476,29 +3467,36 @@ def verify_certs_callback(self, connection, x509, self.log("Client certificate is valid.") return ok - @classmethod - def get_ssl_context(cls): - if cls.ssl_context is None: - if cls.certfile is None: + def get_ssl_context(self): + if self.ssl_context is None: + if self.certfile is None: raise ValueError("at least certfile must be specified") - cls.ssl_context = SSL.Context(cls.ssl_protocol) - if cls.ssl_protocol != SSL.SSLv2_METHOD: - cls.ssl_context.set_options(SSL.OP_NO_SSLv2) + self.ssl_context = SSL.Context(self.ssl_protocol) + if self.ssl_protocol != SSL.SSLv2_METHOD: + self.ssl_context.set_options(SSL.OP_NO_SSLv2) else: warnings.warn("SSLv2 protocol is insecure", RuntimeWarning) - cls.ssl_context.use_certificate_chain_file(cls.certfile) - if not cls.keyfile: - cls.keyfile = cls.certfile - cls.ssl_context.use_privatekey_file(cls.keyfile) - if cls.client_certfile is not None: + self.ssl_context.use_certificate_chain_file(self.certfile) + if not self.keyfile: + self.keyfile = self.certfile + self.ssl_context.use_privatekey_file(self.keyfile) + if self.client_certfile is not None: + from OpenSSL.SSL import VERIFY_CLIENT_ONCE + from OpenSSL.SSL import VERIFY_FAIL_IF_NO_PEER_CERT + from OpenSSL.SSL import VERIFY_PEER + self.ssl_context.set_verify(VERIFY_PEER | + VERIFY_FAIL_IF_NO_PEER_CERT | + VERIFY_CLIENT_ONCE, + self.verify_certs_callback) from OpenSSL.SSL import OP_NO_TICKET from OpenSSL.SSL import SESS_CACHE_OFF - cls.ssl_context.load_verify_locations(cls.client_certfile) - cls.ssl_context.set_session_cache_mode(SESS_CACHE_OFF) - cls.ssl_options = cls.ssl_options | OP_NO_TICKET - if cls.ssl_options: - cls.ssl_context.set_options(cls.ssl_options) - return cls.ssl_context + self.ssl_context.load_verify_locations( + self.client_certfile) + self.ssl_context.set_session_cache_mode(SESS_CACHE_OFF) + self.ssl_options = self.ssl_options | OP_NO_TICKET + if self.ssl_options: + self.ssl_context.set_options(self.ssl_options) + return self.ssl_context # --- overridden methods diff --git a/pyftpdlib/servers.py b/pyftpdlib/servers.py index e703af3f..af623439 100644 --- a/pyftpdlib/servers.py +++ b/pyftpdlib/servers.py @@ -104,8 +104,8 @@ def __init__(self, address_or_socket, handler, ioloop=None, backlog=100): self.ip_map = [] # in case of FTPS class not properly configured we want errors # to be raised here rather than later, when client connects - if hasattr(handler, 'get_ssl_context'): - handler.get_ssl_context() + # if hasattr(handler, 'get_ssl_context'): + # handler.get_ssl_context(handler) if callable(getattr(address_or_socket, 'listen', None)): sock = address_or_socket sock.setblocking(0) diff --git a/pyftpdlib/test/functional_ssl_client_certfile_tests.py b/pyftpdlib/test/functional_ssl_client_certfile_tests.py deleted file mode 100644 index 7c5d93c5..00000000 --- a/pyftpdlib/test/functional_ssl_client_certfile_tests.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env python - -# Copyright (C) 2007-2016 Giampaolo Rodola' . -# Use of this source code is governed by MIT license that can be -# found in the LICENSE file. -# -# -# Does not follow naming convention of other tests because this -# CANNOT be run in the same test suite with test_functional_ssl. -# The test parallelism causes SSL errors when there should be none -# Please run these tests separately - -import ftplib -import os -import sys - -import OpenSSL # requires "pip install pyopenssl" - -from pyftpdlib.handlers import TLS_FTPHandler -from pyftpdlib.test import configure_logging -from pyftpdlib.test import remove_test_files -from pyftpdlib.test import ThreadedTestFTPd -from pyftpdlib.test import TIMEOUT -from pyftpdlib.test import unittest -from pyftpdlib.test import VERBOSITY -from _ssl import SSLError -import ssl - - -FTPS_SUPPORT = hasattr(ftplib, 'FTP_TLS') -if sys.version_info < (2, 7): - FTPS_UNSUPPORT_REASON = "requires python 2.7+" -else: - FTPS_UNSUPPORT_REASON = "FTPS test skipped" - -CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), - 'keycert.pem')) -CLIENT_CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), - 'clientcert.pem')) - -del OpenSSL - - -if FTPS_SUPPORT: - class FTPSClient(ftplib.FTP_TLS): - """A modified version of ftplib.FTP_TLS class which implicitly - secure the data connection after login(). - """ - - def login(self, *args, **kwargs): - ftplib.FTP_TLS.login(self, *args, **kwargs) - self.prot_p() - - class FTPSServerAuth(ThreadedTestFTPd): - """A threaded FTPS server that forces client certificate - authentication used for functional testing. - """ - handler = TLS_FTPHandler - handler.certfile = CERTFILE - handler.client_certfile = CLIENT_CERTFILE - - -# ===================================================================== -# dedicated FTPS tests with client authentication -# ===================================================================== - - -@unittest.skipUnless(FTPS_SUPPORT, FTPS_UNSUPPORT_REASON) -class TestFTPS(unittest.TestCase): - """Specific tests for TLS_FTPHandler class.""" - - def setUp(self): - self.server = FTPSServerAuth() - self.server.start() - - def tearDown(self): - self.client.ssl_version = ssl.PROTOCOL_SSLv23 - with self.server.lock: - self.server.handler.ssl_version = ssl.PROTOCOL_SSLv23 - self.server.handler.tls_control_required = False - self.server.handler.tls_data_required = False - self.client.close() - self.server.stop() - - def assertRaisesWithMsg(self, excClass, msg, callableObj, *args, **kwargs): - try: - callableObj(*args, **kwargs) - except excClass as err: - if str(err) == msg: - return - raise self.failureException("%s != %s" % (str(err), msg)) - else: - if hasattr(excClass, '__name__'): - excName = excClass.__name__ - else: - excName = str(excClass) - raise self.failureException("%s not raised" % excName) - - @classmethod - def get_ssl_context(cls, certfile): - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - if certfile: - ssl_context.load_cert_chain(certfile) - return ssl_context - - def test_auth_client_cert(self): - ctx = self.get_ssl_context(CLIENT_CERTFILE) - self.client = ftplib.FTP_TLS(timeout=TIMEOUT, context=ctx) - self.client.connect(self.server.host, self.server.port) - # secured - try: - self.client.login() - except Exception: - self.fail("login with certificate should work") - - def test_auth_client_nocert(self): - self.client = ftplib.FTP_TLS(timeout=TIMEOUT) - self.client.connect(self.server.host, self.server.port) - try: - self.client.login() - except SSLError as e: - # client should not be able to log in - if "SSLV3_ALERT_HANDSHAKE_FAILURE" in e.reason: - pass - else: - self.fail("Incorrect SSL error with" + - " missing client certificate") - else: - self.fail("Client able to log in with no certificate") - - def test_auth_client_badcert(self): - ctx = self.get_ssl_context(CERTFILE) - self.client = ftplib.FTP_TLS(timeout=TIMEOUT, context=ctx) - self.client.connect(self.server.host, self.server.port) - try: - self.client.login() - except Exception as e: - # client should not be able to log in - if "TLSV1_ALERT_UNKNOWN_CA" in e.reason: - pass - else: - self.fail("Incorrect SSL error with bad client certificate") - else: - self.fail("Client able to log in with bad certificate") - - -configure_logging() -remove_test_files() - - -if __name__ == '__main__': - unittest.main(verbosity=VERBOSITY) diff --git a/pyftpdlib/test/test_functional.py b/pyftpdlib/test/test_functional.py index 496cf93c..9c7214a4 100644 --- a/pyftpdlib/test/test_functional.py +++ b/pyftpdlib/test/test_functional.py @@ -2132,7 +2132,7 @@ class _TestNetworkProtocols(object): HOST = HOST def setUp(self): - self.server = self.server_class((self.HOST, 0)) + self.server = self.server_class(addr=(self.HOST, 0)) self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) diff --git a/pyftpdlib/test/test_functional_ssl.py b/pyftpdlib/test/test_functional_ssl.py index c80b1e7e..2fee34e0 100644 --- a/pyftpdlib/test/test_functional_ssl.py +++ b/pyftpdlib/test/test_functional_ssl.py @@ -10,6 +10,7 @@ import socket import sys import ssl +from ssl import SSLError import OpenSSL # requires "pip install pyopenssl" @@ -49,6 +50,8 @@ CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), 'keycert.pem')) +CLIENT_CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), + 'clientcert.pem')) del OpenSSL @@ -78,6 +81,13 @@ class FTPSServer(ThreadedTestFTPd): handler = TLS_FTPHandler handler.certfile = CERTFILE + def __init__(self, use_client_cert=False, *args, **kwargs): + if use_client_cert: + self.handler.client_certfile = CLIENT_CERTFILE + else: + self.handler.client_certfile = None + super(FTPSServer, self).__init__(*args, **kwargs) + class TLSTestMixin: server_class = FTPSServer client_class = FTPSClient @@ -408,6 +418,88 @@ def test_sslv2(self): self.client.ssl_version = ssl.PROTOCOL_SSLv2 +@unittest.skipUnless(FTPS_SUPPORT, FTPS_UNSUPPORT_REASON) +class TestClientFTPS(unittest.TestCase): + """Specific tests for TLS_FTPHandler class.""" + + def setUp(self): + self.server = FTPSServer(use_client_cert=True) + self.server.start() + + def tearDown(self): + self.client.ssl_version = ssl.PROTOCOL_SSLv23 + with self.server.lock: + self.server.handler.ssl_version = ssl.PROTOCOL_SSLv23 + self.server.handler.tls_control_required = False + self.server.handler.tls_data_required = False + self.server.handler.client_certfile = None + self.client.close() + self.server.stop() + + def assertRaisesWithMsg(self, excClass, msg, callableObj, *args, **kwargs): + try: + callableObj(*args, **kwargs) + except excClass as err: + if str(err) == msg: + return + raise self.failureException("%s != %s" % (str(err), msg)) + else: + if hasattr(excClass, '__name__'): + excName = excClass.__name__ + else: + excName = str(excClass) + raise self.failureException("%s not raised" % excName) + + @classmethod + def get_ssl_context(cls, certfile): + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + if certfile: + ssl_context.load_cert_chain(certfile) + return ssl_context + + def test_auth_client_cert(self): + ctx = self.get_ssl_context(CLIENT_CERTFILE) + self.client = ftplib.FTP_TLS(timeout=TIMEOUT, context=ctx) + self.client.connect(self.server.host, self.server.port) + # secured + try: + self.client.login() + except Exception: + self.fail("login with certificate should work") + + def test_auth_client_nocert(self): + self.client = ftplib.FTP_TLS(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + try: + self.client.login() + except SSLError as e: + # client should not be able to log in + if "SSLV3_ALERT_HANDSHAKE_FAILURE" in e.reason: + pass + else: + self.fail("Incorrect SSL error with" + + " missing client certificate") + else: + self.fail("Client able to log in with no certificate") + + def test_auth_client_badcert(self): + ctx = self.get_ssl_context(CERTFILE) + self.client = ftplib.FTP_TLS(timeout=TIMEOUT, context=ctx) + self.client.connect(self.server.host, self.server.port) + try: + self.client.login() + except Exception as e: + # client should not be able to log in + if "TLSV1_ALERT_UNKNOWN_CA" in e.reason: + pass + else: + self.fail("Incorrect SSL error with bad client certificate") + else: + self.fail("Client able to log in with bad certificate") + + configure_logging() remove_test_files() From 6b4aae7d0f6729376b3078576546a23997711f9f Mon Sep 17 00:00:00 2001 From: Zuo Haocheng Date: Mon, 17 Jul 2017 15:29:56 +0800 Subject: [PATCH 4/7] Bring back SSL option validation on startup by introducing handlers.validate_ssl_options --- pyftpdlib/handlers.py | 44 +++++++++++++++------------ pyftpdlib/servers.py | 4 +-- pyftpdlib/test/test_functional_ssl.py | 6 ++-- setup.py | 1 + 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/pyftpdlib/handlers.py b/pyftpdlib/handlers.py index a3883658..d668a7dc 100644 --- a/pyftpdlib/handlers.py +++ b/pyftpdlib/handlers.py @@ -3469,17 +3469,7 @@ def verify_certs_callback(self, connection, x509, def get_ssl_context(self): if self.ssl_context is None: - if self.certfile is None: - raise ValueError("at least certfile must be specified") - self.ssl_context = SSL.Context(self.ssl_protocol) - if self.ssl_protocol != SSL.SSLv2_METHOD: - self.ssl_context.set_options(SSL.OP_NO_SSLv2) - else: - warnings.warn("SSLv2 protocol is insecure", RuntimeWarning) - self.ssl_context.use_certificate_chain_file(self.certfile) - if not self.keyfile: - self.keyfile = self.certfile - self.ssl_context.use_privatekey_file(self.keyfile) + self.ssl_context = self.validate_ssl_options() if self.client_certfile is not None: from OpenSSL.SSL import VERIFY_CLIENT_ONCE from OpenSSL.SSL import VERIFY_FAIL_IF_NO_PEER_CERT @@ -3488,16 +3478,32 @@ def get_ssl_context(self): VERIFY_FAIL_IF_NO_PEER_CERT | VERIFY_CLIENT_ONCE, self.verify_certs_callback) - from OpenSSL.SSL import OP_NO_TICKET - from OpenSSL.SSL import SESS_CACHE_OFF - self.ssl_context.load_verify_locations( - self.client_certfile) - self.ssl_context.set_session_cache_mode(SESS_CACHE_OFF) - self.ssl_options = self.ssl_options | OP_NO_TICKET - if self.ssl_options: - self.ssl_context.set_options(self.ssl_options) return self.ssl_context + @classmethod + def validate_ssl_options(cls): + if cls.certfile is None: + raise ValueError("at least certfile must be specified") + ssl_context = SSL.Context(cls.ssl_protocol) + if cls.ssl_protocol != SSL.SSLv2_METHOD: + ssl_context.set_options(SSL.OP_NO_SSLv2) + else: + warnings.warn("SSLv2 protocol is insecure", RuntimeWarning) + ssl_context.use_certificate_chain_file(cls.certfile) + if not cls.keyfile: + cls.keyfile = cls.certfile + ssl_context.use_privatekey_file(cls.keyfile) + if cls.client_certfile is not None: + from OpenSSL.SSL import OP_NO_TICKET + from OpenSSL.SSL import SESS_CACHE_OFF + ssl_context.load_verify_locations( + cls.client_certfile) + ssl_context.set_session_cache_mode(SESS_CACHE_OFF) + cls.ssl_options = cls.ssl_options | OP_NO_TICKET + if cls.ssl_options: + ssl_context.set_options(cls.ssl_options) + return ssl_context + # --- overridden methods def flush_account(self): diff --git a/pyftpdlib/servers.py b/pyftpdlib/servers.py index af623439..27b9084d 100644 --- a/pyftpdlib/servers.py +++ b/pyftpdlib/servers.py @@ -104,8 +104,8 @@ def __init__(self, address_or_socket, handler, ioloop=None, backlog=100): self.ip_map = [] # in case of FTPS class not properly configured we want errors # to be raised here rather than later, when client connects - # if hasattr(handler, 'get_ssl_context'): - # handler.get_ssl_context(handler) + if hasattr(handler, 'validate_ssl_options'): + handler.validate_ssl_options() if callable(getattr(address_or_socket, 'listen', None)): sock = address_or_socket sock.setblocking(0) diff --git a/pyftpdlib/test/test_functional_ssl.py b/pyftpdlib/test/test_functional_ssl.py index 2fee34e0..382c5fc0 100644 --- a/pyftpdlib/test/test_functional_ssl.py +++ b/pyftpdlib/test/test_functional_ssl.py @@ -370,7 +370,7 @@ def try_protocol_combo(self, server_protocol, client_protocol): # for proto in protos: # self.try_protocol_combo(ssl.PROTOCOL_TLSv1, proto) - # On OSX TLS_FTPHandler.get_ssl_context()._context does not exist. + # On OSX TLS_FTPHandler.validate_ssl_options()._context does not exist. @unittest.skipIf(OSX, "can't get options on OSX") def test_ssl_options(self): from OpenSSL import SSL @@ -378,7 +378,7 @@ def test_ssl_options(self): from pyftpdlib.handlers import TLS_FTPHandler try: TLS_FTPHandler.ssl_context = None - ctx = TLS_FTPHandler.get_ssl_context() + ctx = TLS_FTPHandler.validate_ssl_options() # Verify default opts. with contextlib.closing(socket.socket()) as s: s = SSL.Connection(ctx, s) @@ -392,7 +392,7 @@ def test_ssl_options(self): # ssl_proto is set to SSL.SSLv23_METHOD). TLS_FTPHandler.ssl_context = None TLS_FTPHandler.ssl_options = None - ctx = TLS_FTPHandler.get_ssl_context() + ctx = TLS_FTPHandler.validate_ssl_options() with contextlib.closing(socket.socket()) as s: s = SSL.Connection(ctx, s) opts = lib.SSL_CTX_get_options(ctx._context) diff --git a/setup.py b/setup.py index dadea829..d0674972 100644 --- a/setup.py +++ b/setup.py @@ -89,6 +89,7 @@ def main(): "pyftpdlib.test": [ "README", 'keycert.pem', + 'clientcert.pem', ], }, keywords=['ftp', 'ftps', 'server', 'ftpd', 'daemon', 'python', 'ssl', From 5c2e1f45909ca641f4c058c47d83f6c5bddcefb1 Mon Sep 17 00:00:00 2001 From: Zuo Haocheng Date: Mon, 17 Jul 2017 16:42:00 +0800 Subject: [PATCH 5/7] Change back to ftplib.FTP_TLS:certfile as :context is not supported on Travis --- pyftpdlib/test/test_functional_ssl.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/pyftpdlib/test/test_functional_ssl.py b/pyftpdlib/test/test_functional_ssl.py index 382c5fc0..2d95dcb9 100644 --- a/pyftpdlib/test/test_functional_ssl.py +++ b/pyftpdlib/test/test_functional_ssl.py @@ -450,18 +450,9 @@ def assertRaisesWithMsg(self, excClass, msg, callableObj, *args, **kwargs): excName = str(excClass) raise self.failureException("%s not raised" % excName) - @classmethod - def get_ssl_context(cls, certfile): - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - if certfile: - ssl_context.load_cert_chain(certfile) - return ssl_context - def test_auth_client_cert(self): - ctx = self.get_ssl_context(CLIENT_CERTFILE) - self.client = ftplib.FTP_TLS(timeout=TIMEOUT, context=ctx) + self.client = ftplib.FTP_TLS(timeout=TIMEOUT, + certfile=CLIENT_CERTFILE) self.client.connect(self.server.host, self.server.port) # secured try: @@ -485,8 +476,7 @@ def test_auth_client_nocert(self): self.fail("Client able to log in with no certificate") def test_auth_client_badcert(self): - ctx = self.get_ssl_context(CERTFILE) - self.client = ftplib.FTP_TLS(timeout=TIMEOUT, context=ctx) + self.client = ftplib.FTP_TLS(timeout=TIMEOUT, certfile=CERTFILE) self.client.connect(self.server.host, self.server.port) try: self.client.login() From 930a60af39cc438ae01bce1dcfdcc1a6fa3a087b Mon Sep 17 00:00:00 2001 From: Zuo Haocheng Date: Tue, 25 Jul 2017 20:54:59 +0800 Subject: [PATCH 6/7] Updates per code review --- docs/api.rst | 6 +++--- pyftpdlib/handlers.py | 9 ++++----- pyftpdlib/servers.py | 4 ++-- pyftpdlib/test/test_functional_ssl.py | 6 +++--- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index e363a6bf..c149aa88 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -559,9 +559,9 @@ Extended handlers .. data:: client_certfile - The path of the certificate to check the client certificate against. - When provided, only allowing clients with a valid certificate to connect - to the server (default ``None``). + The path to a file which contains a certificate to be used to identify + the client. If specified, only clients with a valid certificate are able + to connect to the server (default ``None``). .. versionadded:: 1.5.3 diff --git a/pyftpdlib/handlers.py b/pyftpdlib/handlers.py index d668a7dc..e74c0e56 100644 --- a/pyftpdlib/handlers.py +++ b/pyftpdlib/handlers.py @@ -3419,7 +3419,6 @@ class TLS_FTPHandler(SSLConnection, FTPHandler): certfile = None keyfile = None ssl_protocol = SSL.SSLv23_METHOD - # client certificate configurable attributes client_certfile = None # - SSLv2 is easily broken and is considered harmful and dangerous # - SSLv3 has several problems and is now dangerous @@ -3454,7 +3453,7 @@ def __init__(self, conn, server, ioloop=None): self._extra_feats = ['AUTH TLS', 'AUTH SSL', 'PBSZ', 'PROT'] self._pbsz = False self._prot = False - self.ssl_context = self.get_ssl_context() + self.init_ssl_context() def __repr__(self): return FTPHandler.__repr__(self) @@ -3467,9 +3466,9 @@ def verify_certs_callback(self, connection, x509, self.log("Client certificate is valid.") return ok - def get_ssl_context(self): + def init_ssl_context(self): if self.ssl_context is None: - self.ssl_context = self.validate_ssl_options() + self.ssl_context = self.get_ssl_context() if self.client_certfile is not None: from OpenSSL.SSL import VERIFY_CLIENT_ONCE from OpenSSL.SSL import VERIFY_FAIL_IF_NO_PEER_CERT @@ -3481,7 +3480,7 @@ def get_ssl_context(self): return self.ssl_context @classmethod - def validate_ssl_options(cls): + def get_ssl_context(cls): if cls.certfile is None: raise ValueError("at least certfile must be specified") ssl_context = SSL.Context(cls.ssl_protocol) diff --git a/pyftpdlib/servers.py b/pyftpdlib/servers.py index 27b9084d..e703af3f 100644 --- a/pyftpdlib/servers.py +++ b/pyftpdlib/servers.py @@ -104,8 +104,8 @@ def __init__(self, address_or_socket, handler, ioloop=None, backlog=100): self.ip_map = [] # in case of FTPS class not properly configured we want errors # to be raised here rather than later, when client connects - if hasattr(handler, 'validate_ssl_options'): - handler.validate_ssl_options() + if hasattr(handler, 'get_ssl_context'): + handler.get_ssl_context() if callable(getattr(address_or_socket, 'listen', None)): sock = address_or_socket sock.setblocking(0) diff --git a/pyftpdlib/test/test_functional_ssl.py b/pyftpdlib/test/test_functional_ssl.py index 2d95dcb9..988e8eb0 100644 --- a/pyftpdlib/test/test_functional_ssl.py +++ b/pyftpdlib/test/test_functional_ssl.py @@ -370,7 +370,7 @@ def try_protocol_combo(self, server_protocol, client_protocol): # for proto in protos: # self.try_protocol_combo(ssl.PROTOCOL_TLSv1, proto) - # On OSX TLS_FTPHandler.validate_ssl_options()._context does not exist. + # On OSX TLS_FTPHandler.get_ssl_context()._context does not exist. @unittest.skipIf(OSX, "can't get options on OSX") def test_ssl_options(self): from OpenSSL import SSL @@ -378,7 +378,7 @@ def test_ssl_options(self): from pyftpdlib.handlers import TLS_FTPHandler try: TLS_FTPHandler.ssl_context = None - ctx = TLS_FTPHandler.validate_ssl_options() + ctx = TLS_FTPHandler.get_ssl_context() # Verify default opts. with contextlib.closing(socket.socket()) as s: s = SSL.Connection(ctx, s) @@ -392,7 +392,7 @@ def test_ssl_options(self): # ssl_proto is set to SSL.SSLv23_METHOD). TLS_FTPHandler.ssl_context = None TLS_FTPHandler.ssl_options = None - ctx = TLS_FTPHandler.validate_ssl_options() + ctx = TLS_FTPHandler.get_ssl_context() with contextlib.closing(socket.socket()) as s: s = SSL.Connection(ctx, s) opts = lib.SSL_CTX_get_options(ctx._context) From 992a9b7dc60cd95e94d4bf4641dd72375adef8e4 Mon Sep 17 00:00:00 2001 From: Zuo Haocheng Date: Thu, 27 Jul 2017 11:21:59 +0800 Subject: [PATCH 7/7] Remove check for SSL.OP_NO_SSLv2, as it is removed in OpenSSL --- pyftpdlib/test/test_functional_ssl.py | 42 ++++++++++++--------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/pyftpdlib/test/test_functional_ssl.py b/pyftpdlib/test/test_functional_ssl.py index 988e8eb0..85221234 100644 --- a/pyftpdlib/test/test_functional_ssl.py +++ b/pyftpdlib/test/test_functional_ssl.py @@ -376,31 +376,27 @@ def test_ssl_options(self): from OpenSSL import SSL from OpenSSL._util import lib from pyftpdlib.handlers import TLS_FTPHandler - try: - TLS_FTPHandler.ssl_context = None - ctx = TLS_FTPHandler.get_ssl_context() - # Verify default opts. - with contextlib.closing(socket.socket()) as s: - s = SSL.Connection(ctx, s) - opts = lib.SSL_CTX_get_options(ctx._context) + ctx = TLS_FTPHandler.get_ssl_context() + # Verify default opts. + with contextlib.closing(socket.socket()) as s: + s = SSL.Connection(ctx, s) + opts = lib.SSL_CTX_get_options(ctx._context) + if SSL.OP_NO_SSLv2 != 0: self.assertTrue(opts & SSL.OP_NO_SSLv2) - self.assertTrue(opts & SSL.OP_NO_SSLv3) - self.assertTrue(opts & SSL.OP_NO_COMPRESSION) - TLS_FTPHandler.ssl_context = None # reset - # Make sure that if ssl_options is None no options are set - # (except OP_NO_SSLv2 whch is enabled by default unless - # ssl_proto is set to SSL.SSLv23_METHOD). - TLS_FTPHandler.ssl_context = None - TLS_FTPHandler.ssl_options = None - ctx = TLS_FTPHandler.get_ssl_context() - with contextlib.closing(socket.socket()) as s: - s = SSL.Connection(ctx, s) - opts = lib.SSL_CTX_get_options(ctx._context) + self.assertTrue(opts & SSL.OP_NO_SSLv3) + self.assertTrue(opts & SSL.OP_NO_COMPRESSION) + # Make sure that if ssl_options is None no options are set + # (except OP_NO_SSLv2 whch is enabled by default unless + # ssl_proto is set to SSL.SSLv23_METHOD). + TLS_FTPHandler.ssl_options = None + ctx = TLS_FTPHandler.get_ssl_context() + with contextlib.closing(socket.socket()) as s: + s = SSL.Connection(ctx, s) + opts = lib.SSL_CTX_get_options(ctx._context) + if SSL.OP_NO_SSLv2 != 0: self.assertTrue(opts & SSL.OP_NO_SSLv2) - # self.assertFalse(opts & SSL.OP_NO_SSLv3) - self.assertFalse(opts & SSL.OP_NO_COMPRESSION) - finally: - TLS_FTPHandler.ssl_context = None + # self.assertFalse(opts & SSL.OP_NO_SSLv3) + # self.assertFalse(opts & SSL.OP_NO_COMPRESSION) if hasattr(ssl, "PROTOCOL_SSLv2"): def test_sslv2(self):