From 41ce1a1538293cb6b214ba1f68e66aedbca1cdb3 Mon Sep 17 00:00:00 2001 From: Joerg Steffens Date: Mon, 19 Oct 2020 16:02:30 +0200 Subject: [PATCH 01/10] example: fix Current versions of sslpsk requires identity and password as bytes, not strings. This commit adapts the example scripts. --- sslpsk/test/example_client.py | 6 ++++-- sslpsk/test/example_server.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/sslpsk/test/example_client.py b/sslpsk/test/example_client.py index 155806c..43f4596 100644 --- a/sslpsk/test/example_client.py +++ b/sslpsk/test/example_client.py @@ -3,8 +3,10 @@ import ssl import sslpsk -PSKS = {'server1' : b'abcdef', - 'server2' : b'uvwxyz'} +PSKS = { + b'server1' : b'abcdef', + b'server2' : b'uvwxyz', +} def client(host, port, psk): tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff --git a/sslpsk/test/example_server.py b/sslpsk/test/example_server.py index 73b3449..af474f3 100644 --- a/sslpsk/test/example_server.py +++ b/sslpsk/test/example_server.py @@ -3,8 +3,10 @@ import ssl import sslpsk -PSKS = {'client1' : b'abcdef', - 'client2' : b'123456'} +PSKS = { + b'client1' : b'abcdef', + b'client2' : b'123456', + } def server(host, port): tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) From d4be0903df8485bb0f4e9837776e85146ce8e559 Mon Sep 17 00:00:00 2001 From: Joerg Steffens Date: Fri, 16 Oct 2020 18:52:33 +0200 Subject: [PATCH 02/10] unittest: exit with exitcode >0 on failure Without this change, a test failure will be displayed, but not recognized as error by travis or other tools. --- sslpsk/test/__init__.py | 3 ++- sslpsk/test/__main__.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/sslpsk/test/__init__.py b/sslpsk/test/__init__.py index aa884a6..b3d4559 100644 --- a/sslpsk/test/__init__.py +++ b/sslpsk/test/__init__.py @@ -19,4 +19,5 @@ def tests(): return unittest.TestLoader().discover(os.path.dirname(__file__)) def run(): - unittest.TextTestRunner(verbosity=1).run(tests()) + return unittest.TextTestRunner(verbosity=1).run(tests()) + diff --git a/sslpsk/test/__main__.py b/sslpsk/test/__main__.py index de729ce..3cdf233 100644 --- a/sslpsk/test/__main__.py +++ b/sslpsk/test/__main__.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License +import sys import sslpsk.test -sslpsk.test.run() +result = sslpsk.test.run() +sys.exit(not result.wasSuccessful()) From 40f18a7a1a2e91d95fd41d283d4d4d03bd319345 Mon Sep 17 00:00:00 2001 From: Joerg Steffens Date: Thu, 29 Oct 2020 16:30:58 +0100 Subject: [PATCH 03/10] sslpsk: cleanup --- sslpsk/sslpsk.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/sslpsk/sslpsk.py b/sslpsk/sslpsk.py index bd0f12b..caef91d 100644 --- a/sslpsk/sslpsk.py +++ b/sslpsk/sslpsk.py @@ -86,11 +86,8 @@ def wrap_socket(*args, **kwargs): do_handshake_on_connect = kwargs.get('do_handshake_on_connect', True) kwargs['do_handshake_on_connect'] = False - psk = kwargs.setdefault('psk', None) - del kwargs['psk'] - - hint = kwargs.setdefault('hint', None) - del kwargs['hint'] + psk = kwargs.pop('psk', None) + hint = kwargs.pop('hint', None) server_side = kwargs.setdefault('server_side', False) if psk: From 1f431251afe0c569991942bedf20a2e99b997df1 Mon Sep 17 00:00:00 2001 From: Joerg Steffens Date: Fri, 16 Oct 2020 17:00:27 +0200 Subject: [PATCH 04/10] travis: added python 3.8 for testing And execute unittest in verbose and buffered mode. --- .travis.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d358684..528a860 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,9 @@ matrix: - os: linux python: 3.6 - os: linux - python: 3.7-dev + python: 3.7 + - os: linux + python: 3.8 - os: osx language: generic env: PYTHON_BIN=python2 @@ -21,4 +23,5 @@ matrix: script: - $PYTHON_BIN setup.py install - cd .. - - $PYTHON_BIN -m sslpsk.test + - # $PYTHON_BIN -m sslpsk.test + - $PYTHON_BIN sslpsk/sslpsk/test/test_sslpsk.py -v -b From 1a7460d0ab1663a196aab94b07daf85ac26b9361 Mon Sep 17 00:00:00 2001 From: Joerg Steffens Date: Mon, 19 Oct 2020 17:45:47 +0200 Subject: [PATCH 05/10] unittest: added more test cases Added tests for different ciphers and different TLS Protocol versions. Unfortenatlty, the TLS Protocol setting "ssl.PROTOCOL_TLS", which is the Python ssl module default and with which the ssl modul should choose the best matching protocol version fails for the sslpsk client side. Therefore we mark the test using this as "expected failure". To improve readablity of the test results, Python warnings will not be displayed (warnings="ignored"). Also test should best be started in buffered mode ("-b" on command line). Code is formated by the tool "black". --- sslpsk/test/test_sslpsk.py | 152 +++++++++++++++++++++++++++++-------- 1 file changed, 120 insertions(+), 32 deletions(-) diff --git a/sslpsk/test/test_sslpsk.py b/sslpsk/test/test_sslpsk.py index 6913525..efd6b82 100644 --- a/sslpsk/test/test_sslpsk.py +++ b/sslpsk/test/test_sslpsk.py @@ -18,75 +18,163 @@ import sslpsk import sys import threading +import traceback import unittest -HOST='localhost' -PORT=6000 -TEST_DATA=b'abcdefghi' +HOST = "localhost" +PORT = 6000 +TEST_DATA = b"abcdefghi" +CIPHERS = "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH" + class SSLPSKTest(unittest.TestCase): # ---------- setup/tear down functions def setUp(self): - self.psk = b'c033f52671c61c8128f7f8a40be88038bcf2b07a6eb3095c36e3759f0cf40837' + self.key = b"c033f52671c61c8128f7f8a40be88038bcf2b07a6eb3095c36e3759f0cf40837" + self.client_psk = self.key + self.server_psk = self.key self.addr = (HOST, PORT) self.client_socket = socket.socket() self.server_socket = None self.accept_socket = socket.socket() self.client_psk_sock = None self.server_psk_sock = None + self.server_cipher = None + self.server_ssl_version = None + self.server_thread = None + self.server_thread_exception = None + self.server_thread_traceback = None - self.startServer() - def tearDown(self): - for sock in [self.client_psk_sock or self.client_socket, - self.server_psk_sock or self.server_socket, - self.accept_socket]: - try: - sock.shutdown(socket.SHUT_RDWR) - except socket.error: - pass - finally: - sock.close() + if self.server_thread_exception is not None: + print("Traceback from server thread ({}):\n".format(self.id())) + print(self.server_thread_traceback) + print(self.server_thread_exception) + for sock in [ + self.client_psk_sock or self.client_socket, + self.server_psk_sock or self.server_socket, + self.accept_socket, + ]: + if sock is not None: + try: + sock.shutdown(socket.SHUT_RDWR) + except socket.error: + pass + finally: + sock.close() self.client_socket = None self.server_socket = None self.accept_socket = None self.client_psk_sock = None self.server_psk_sock = None + if self.server_thread is not None: + self.server_thread.join() + self.server_thread = None - def startServer(self): + def startServer(self, ssl_version=ssl.PROTOCOL_TLSv1, ciphers=CIPHERS, myid=None): self.accept_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.accept_socket.bind(self.addr) self.accept_socket.listen(1) + self.server_ciphers = ciphers + self.server_ssl_version = ssl_version + self.server_id = myid def accept(): - self.server_socket, _ = self.accept_socket.accept() - - # wrap socket with TLS-PSK - self.server_psk_sock = sslpsk.wrap_socket(self.server_socket, psk=self.psk, ciphers='PSK-AES256-CBC-SHA', - ssl_version=ssl.PROTOCOL_TLSv1, server_side=True) - + try: + self.server_socket, _ = self.accept_socket.accept() + except Exception as exc: + self.server_thread_exception = exc + self.server_thread_traceback = "".join( + traceback.format_tb(exc.__traceback__) + ) + return + + try: + # wrap socket with TLS-PSK + self.server_psk_sock = sslpsk.wrap_socket( + self.server_socket, + psk=self.server_psk, + ciphers=self.server_ciphers, + ssl_version=self.server_ssl_version, + server_side=True, + hint=self.server_id, + ) + except Exception as exc: + self.server_thread_exception = exc + self.server_thread_traceback = "".join( + traceback.format_tb(exc.__traceback__) + ) + return + # accept data from client data = self.server_psk_sock.recv(10) self.server_psk_sock.sendall(data.upper()) - threading.Thread(target = accept).start() + self.server_thread = threading.Thread(target=accept) + self.server_thread.start() - def testClient(self): + def connectAndReceiveData( + self, ssl_version=ssl.PROTOCOL_TLSv1, ciphers=CIPHERS, myid=None + ): # initialize self.client_socket.connect(self.addr) - + # wrap socket with TLS-PSK - self.client_psk_sock = sslpsk.wrap_socket(self.client_socket, psk=self.psk, ciphers='PSK-AES256-CBC-SHA', - ssl_version=ssl.PROTOCOL_TLSv1, server_side=False) - + self.client_psk_sock = sslpsk.wrap_socket( + self.client_socket, + psk=self.client_psk, + ciphers=ciphers, + ssl_version=ssl_version, + server_side=False, + hint=myid, + ) + self.client_psk_sock.sendall(TEST_DATA) data = self.client_psk_sock.recv(10) - print('data: %s' % data) - self.assertTrue(data == TEST_DATA.upper(), 'Test Failed') + self.assertTrue(data == TEST_DATA.upper(), "Test Failed") + + def testClientCiphersPskAes256(self): + ciphers = "PSK-AES256-CBC-SHA" + ssl_version = ssl.PROTOCOL_TLSv1 + self.startServer(ssl_version, ciphers) + self.connectAndReceiveData(ssl_version, ciphers) + + def testCiphersAll(self): + ciphers = "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH" + ssl_version = ssl.PROTOCOL_TLSv1 + self.startServer(ssl_version, ciphers) + self.connectAndReceiveData(ssl_version, ciphers) + + @unittest.expectedFailure + def testProtocolTls(self): + ssl_version = ssl.PROTOCOL_TLS + self.startServer(ssl_version=ssl_version) + self.connectAndReceiveData(ssl_version=ssl_version) + + def testProtocolTlsV1(self): + ssl_version = ssl.PROTOCOL_TLSv1 + self.startServer(ssl_version=ssl_version) + self.connectAndReceiveData(ssl_version=ssl_version) + + def testProtocolTlsV1_1(self): + ssl_version = ssl.PROTOCOL_TLSv1_1 + self.startServer(ssl_version=ssl_version) + self.connectAndReceiveData(ssl_version=ssl_version) + + def testProtocolTlsV1_2(self): + ssl_version = ssl.PROTOCOL_TLSv1_2 + self.startServer(ssl_version=ssl_version) + self.connectAndReceiveData(ssl_version=ssl_version) + + def testProtocolClientTlsV1_2ServerTls(self): + self.startServer(ssl_version=ssl.PROTOCOL_TLS) + self.connectAndReceiveData(ssl_version=ssl.PROTOCOL_TLSv1_2) + def main(): - unittest.main(buffer=False) + unittest.main(warnings="ignore") + -if __name__ == '__main__': +if __name__ == "__main__": main() From 12a27033395f6fc274bdc73be974f4c36e75aeb5 Mon Sep 17 00:00:00 2001 From: Joerg Steffens Date: Thu, 29 Oct 2020 17:31:24 +0100 Subject: [PATCH 06/10] unittest: test different identities Test multiple identities with different PSKs. Also switched default TLS protocol from ssl.PROTOCOL_TLSv1 to ssl.PROTOCOL_TLSv1_2 as this also works with Ubuntu 20.04. --- sslpsk/test/test_sslpsk.py | 42 ++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/sslpsk/test/test_sslpsk.py b/sslpsk/test/test_sslpsk.py index efd6b82..c9be781 100644 --- a/sslpsk/test/test_sslpsk.py +++ b/sslpsk/test/test_sslpsk.py @@ -72,7 +72,7 @@ def tearDown(self): self.server_thread.join() self.server_thread = None - def startServer(self, ssl_version=ssl.PROTOCOL_TLSv1, ciphers=CIPHERS, myid=None): + def startServer(self, ssl_version=ssl.PROTOCOL_TLSv1_2, ciphers=CIPHERS, myid=None): self.accept_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.accept_socket.bind(self.addr) self.accept_socket.listen(1) @@ -115,7 +115,7 @@ def accept(): self.server_thread.start() def connectAndReceiveData( - self, ssl_version=ssl.PROTOCOL_TLSv1, ciphers=CIPHERS, myid=None + self, ssl_version=ssl.PROTOCOL_TLSv1_2, ciphers=CIPHERS, myid=None ): # initialize self.client_socket.connect(self.addr) @@ -136,13 +136,13 @@ def connectAndReceiveData( def testClientCiphersPskAes256(self): ciphers = "PSK-AES256-CBC-SHA" - ssl_version = ssl.PROTOCOL_TLSv1 + ssl_version = ssl.PROTOCOL_TLSv1_2 self.startServer(ssl_version, ciphers) self.connectAndReceiveData(ssl_version, ciphers) def testCiphersAll(self): ciphers = "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH" - ssl_version = ssl.PROTOCOL_TLSv1 + ssl_version = ssl.PROTOCOL_TLSv1_2 self.startServer(ssl_version, ciphers) self.connectAndReceiveData(ssl_version, ciphers) @@ -171,6 +171,40 @@ def testProtocolClientTlsV1_2ServerTls(self): self.startServer(ssl_version=ssl.PROTOCOL_TLS) self.connectAndReceiveData(ssl_version=ssl.PROTOCOL_TLSv1_2) + def testIdentity(self): + identity = b"id1" + psks = {b"id1": b"abcdef", b"id2": b"123456"} + self.server_psk = lambda identity: psks.get(identity) + self.client_psk = (b"abcdef", b"id1") + + self.startServer(myid=identity) + self.connectAndReceiveData(myid=identity) + + def testClientIdentity(self): + psks = {b"client1": b"abcdef", b"client2": b"123456"} + self.server_psk = lambda identity: psks.get(identity) + self.client_psk = (b"abcdef", b"client1") + + self.startServer(myid=b"server1") + self.connectAndReceiveData(myid=b"client1") + + def testClientAndServerIdentities(self): + psks_on_server = {b"client1": b"abcdef", b"client2": b"123456"} + self.server_psk = lambda identity: psks_on_server.get(identity) + + id_on_server = {b"server1": b"client1", b"server2": b"client2"} + + psks_on_client = {b"server1": b"abcdef", b"server2": b"123456"} + + self.client_psk = lambda hint: ( + psks_on_client.get(hint), + id_on_server.get(hint), + ) + + self.startServer(myid=b"server1") + self.connectAndReceiveData() + + def main(): unittest.main(warnings="ignore") From d037e99f84e4723846dc8ddcb9067dd8726cf167 Mon Sep 17 00:00:00 2001 From: Joerg Steffens Date: Thu, 29 Oct 2020 17:34:20 +0100 Subject: [PATCH 07/10] unittest: test identities as used by the Bareos project The Bareos backup software also uses TLS-PSK and offers a Python module to access the backup server. As their identity uses a special format (TEXT + recordseperator (0x1E) + TEXT) this test verifies, that this format does not cause problems inside sslpsk. --- sslpsk/test/test_sslpsk.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/sslpsk/test/test_sslpsk.py b/sslpsk/test/test_sslpsk.py index c9be781..e85c90e 100644 --- a/sslpsk/test/test_sslpsk.py +++ b/sslpsk/test/test_sslpsk.py @@ -204,6 +204,24 @@ def testClientAndServerIdentities(self): self.startServer(myid=b"server1") self.connectAndReceiveData() + def testBareosIdentity(self): + def getBareosIdentity(name): + identity_prefix = b"R_CONSOLE" + record_separator = bytes.fromhex("1E") + + result = identity_prefix + record_separator + name + + return bytes(result) + + clientid = getBareosIdentity(b"client1") + + psks = {b"client1": b"abcdef", b"client2": b"123456", clientid: b"secret"} + self.server_psk = lambda identity: psks.get(identity) + self.client_psk = (b"secret", clientid) + + self.startServer() + self.connectAndReceiveData() + def main(): From d9b8dd70355e5baaf9dc4dfd15ef17a59d3465f0 Mon Sep 17 00:00:00 2001 From: Joerg Steffens Date: Thu, 29 Oct 2020 17:44:47 +0100 Subject: [PATCH 08/10] unittest: add seperated tests for client and server part Test the client and server part of sslpsk independently. This makes it easier to find potential problems. The corresponding part is handled by the openssl binary ("openssl s_client" resp. "openssl s_server"). --- sslpsk/test/test_sslpsk.py | 219 +++++++++++++++++++++++++++++++++++-- 1 file changed, 212 insertions(+), 7 deletions(-) diff --git a/sslpsk/test/test_sslpsk.py b/sslpsk/test/test_sslpsk.py index e85c90e..1f37f85 100644 --- a/sslpsk/test/test_sslpsk.py +++ b/sslpsk/test/test_sslpsk.py @@ -12,24 +12,37 @@ # See the License for the specific language governing permissions and # limitations under the License +import binascii import os import socket import ssl import sslpsk +import subprocess import sys import threading +from time import sleep import traceback import unittest +import warnings HOST = "localhost" PORT = 6000 TEST_DATA = b"abcdefghi" CIPHERS = "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH" +TIMEOUT = 3 -class SSLPSKTest(unittest.TestCase): +def cmd_exists(cmd): + return any( + os.access(os.path.join(path, cmd), os.X_OK) + for path in os.environ["PATH"].split(os.pathsep) + ) + + +class SslPskBase(unittest.TestCase): # ---------- setup/tear down functions def setUp(self): + warnings.filterwarnings("ignore") self.key = b"c033f52671c61c8128f7f8a40be88038bcf2b07a6eb3095c36e3759f0cf40837" self.client_psk = self.key self.server_psk = self.key @@ -44,6 +57,7 @@ def setUp(self): self.server_thread = None self.server_thread_exception = None self.server_thread_traceback = None + self.proc = None def tearDown(self): if self.server_thread_exception is not None: @@ -60,6 +74,11 @@ def tearDown(self): sock.shutdown(socket.SHUT_RDWR) except socket.error: pass + except AttributeError: + if sock is None: + pass + else: + raise finally: sock.close() @@ -71,6 +90,12 @@ def tearDown(self): if self.server_thread is not None: self.server_thread.join() self.server_thread = None + if self.proc is not None: + self.proc.stdin.close() + self.proc.stdout.close() + self.proc.stderr.close() + self.proc.terminate() + self.proc = None def startServer(self, ssl_version=ssl.PROTOCOL_TLSv1_2, ciphers=CIPHERS, myid=None): self.accept_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -84,9 +109,10 @@ def accept(): try: self.server_socket, _ = self.accept_socket.accept() except Exception as exc: + exc_type, exc_value, exc_traceback = sys.exc_info() self.server_thread_exception = exc self.server_thread_traceback = "".join( - traceback.format_tb(exc.__traceback__) + traceback.format_tb(exc_traceback) ) return @@ -101,9 +127,10 @@ def accept(): hint=self.server_id, ) except Exception as exc: + exc_type, exc_value, exc_traceback = sys.exc_info() self.server_thread_exception = exc self.server_thread_traceback = "".join( - traceback.format_tb(exc.__traceback__) + traceback.format_tb(exc_traceback) ) return @@ -111,6 +138,15 @@ def accept(): data = self.server_psk_sock.recv(10) self.server_psk_sock.sendall(data.upper()) + # close + try: + self.server_psk_sock.shutdown(socket.SHUT_RDWR) + self.server_psk_sock.close() + except AttributeError: + pass + finally: + self.server_psk_sock = None + self.server_thread = threading.Thread(target=accept) self.server_thread.start() @@ -134,7 +170,13 @@ def connectAndReceiveData( data = self.client_psk_sock.recv(10) self.assertTrue(data == TEST_DATA.upper(), "Test Failed") - def testClientCiphersPskAes256(self): + +class SslPskTest(SslPskBase): + def testClient(self): + self.startServer() + self.connectAndReceiveData() + + def testCiphersPskAes256(self): ciphers = "PSK-AES256-CBC-SHA" ssl_version = ssl.PROTOCOL_TLSv1_2 self.startServer(ssl_version, ciphers) @@ -146,7 +188,12 @@ def testCiphersAll(self): self.startServer(ssl_version, ciphers) self.connectAndReceiveData(ssl_version, ciphers) - @unittest.expectedFailure + @unittest.skipUnless( + hasattr(ssl, "PROTOCOL_TLS"), "ssl module does not provide required protocol" + ) + @unittest.skipIf( + os.environ.get("TRAVIS_OS_NAME") == "osx", "Mac OS is known to fail" + ) def testProtocolTls(self): ssl_version = ssl.PROTOCOL_TLS self.startServer(ssl_version=ssl_version) @@ -167,6 +214,9 @@ def testProtocolTlsV1_2(self): self.startServer(ssl_version=ssl_version) self.connectAndReceiveData(ssl_version=ssl_version) + @unittest.skipUnless( + hasattr(ssl, "PROTOCOL_TLS"), "ssl module does not provide required protocol" + ) def testProtocolClientTlsV1_2ServerTls(self): self.startServer(ssl_version=ssl.PROTOCOL_TLS) self.connectAndReceiveData(ssl_version=ssl.PROTOCOL_TLSv1_2) @@ -207,7 +257,7 @@ def testClientAndServerIdentities(self): def testBareosIdentity(self): def getBareosIdentity(name): identity_prefix = b"R_CONSOLE" - record_separator = bytes.fromhex("1E") + record_separator = bytearray.fromhex("1E") result = identity_prefix + record_separator + name @@ -223,9 +273,164 @@ def getBareosIdentity(name): self.connectAndReceiveData() +@unittest.skipUnless(cmd_exists("openssl"), "openssl program not available") +@unittest.skipIf(sys.version_info < (3, 3), "Python >= 3.3 required") +@unittest.skipIf(os.environ.get("TRAVIS_OS_NAME") == "osx", "Mac OS is not supported") +class SslPskServerTest(SslPskBase): + def connectOpenSslClientWithSslPskServer( + self, ssl_version=ssl.PROTOCOL_TLSv1, ciphers=CIPHERS, myid=None + ): + clientid = b"opensslclient" + psk = b"secret" + psks = {b"client1": b"abcdef", b"client2": b"123456", clientid: psk} + self.server_psk = lambda identity: psks.get(identity) + self.startServer(ssl_version, ciphers, myid) + + command = [ + "openssl", + "s_client", + "-quiet", + "-connect", + "{}:{}".format(HOST, PORT), + "-psk_identity", + clientid, + "-psk", + binascii.hexlify(psk), + ] + self.proc = subprocess.Popen( + command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + # send data from openssl server to python client + out, err = self.proc.communicate(input=TEST_DATA, timeout=TIMEOUT) + + self.assertEqual( + self.proc.returncode, + 0, + "Server command {} exited with error {}.".format( + str(command), self.proc.returncode + ), + ) + self.assertEqual(out, TEST_DATA.upper()) + + @unittest.skipUnless( + hasattr(ssl, "PROTOCOL_TLS"), "ssl module does not provide required protocol" + ) + def testProtocolTls(self): + self.connectOpenSslClientWithSslPskServer(ssl_version=ssl.PROTOCOL_TLS) + + def testProtocolTlsV1(self): + self.connectOpenSslClientWithSslPskServer(ssl_version=ssl.PROTOCOL_TLSv1) + + def testProtocolTlsV1_1(self): + self.connectOpenSslClientWithSslPskServer(ssl_version=ssl.PROTOCOL_TLSv1_1) + + def testProtocolTlsV1_2(self): + self.connectOpenSslClientWithSslPskServer(ssl_version=ssl.PROTOCOL_TLSv1_2) + + +# timeout parameter since Python 3.3 +@unittest.skipUnless(cmd_exists("openssl"), "openssl program not available") +@unittest.skipUnless(sys.version_info >= (3, 3), "Python >= 3.3 required") +class SslPskClientTest(SslPskBase): + def connectSshPskClientWithOpenSslServer( + self, ssl_version=ssl.PROTOCOL_TLSv1, ciphers=CIPHERS, myid=None + ): + # start the openssl server, + # connect sslpsk client to server, + # client: sents data to the server, + # server: sents data to the client and reads incoming data. + clientid = b"pythonclient" + psk = b"secret" + + self.proc = subprocess.Popen( + [ + "openssl", + "s_server", + "-port", + str(PORT), + "-nocert", + "-cipher", + CIPHERS, + "-psk_hint", + b"opensslserver", + "-psk_identity", + clientid, + "-psk", + binascii.hexlify(psk), + ], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + out = "" + err = "" + # send data from openssl server to python client + try: + out, err = self.proc.communicate(input=TEST_DATA.upper(), timeout=1) + except subprocess.TimeoutExpired: + pass + + if self.proc.poll() is not None: + raise unittest.SkipTest( + "openssl is not working: {} {}".format( + out.decode("utf-8")[:100], err.decode("utf-8")[:100] + ) + ) + + # print("out: {}".format(out)) + # print("err: {}".format(err)) + + self.client_socket.connect(self.addr) + # wrap socket with TLS-PSK + self.client_psk_sock = sslpsk.wrap_socket( + self.client_socket, + psk=(psk, clientid), + ssl_version=ssl_version, + ciphers=ciphers, + server_side=False, + hint=myid, + ) + + # Send data from client to server. + self.client_psk_sock.sendall(TEST_DATA) + + # retreive data on the python client + data = self.client_psk_sock.recv(10) + self.assertEqual(data, TEST_DATA.upper()) + + # self.proc.wait(timeout=TIMEOUT) + # send data from openssl server to python client + out, err = self.proc.communicate(timeout=TIMEOUT) + + self.assertEqual(self.proc.returncode, 0) + # Data from client to server is not always retrieved, + # as the server exits very shortly after sending his data. + # Therefore we done check for this. + # self.assertIn(TEST_DATA, out) + + @unittest.skipUnless( + hasattr(ssl, "PROTOCOL_TLS"), "ssl module does not provide required protocol" + ) + def testProtocolTls(self): + self.connectSshPskClientWithOpenSslServer(ssl_version=ssl.PROTOCOL_TLS) + + def testProtocolTlsV1(self): + self.connectSshPskClientWithOpenSslServer(ssl_version=ssl.PROTOCOL_TLSv1) + + def testProtocolTlsV1_1(self): + self.connectSshPskClientWithOpenSslServer(ssl_version=ssl.PROTOCOL_TLSv1_1) + + def testProtocolTlsV1_2(self): + self.connectSshPskClientWithOpenSslServer(ssl_version=ssl.PROTOCOL_TLSv1_2) + def main(): - unittest.main(warnings="ignore") + unittest.main() if __name__ == "__main__": From 762f130aa59c60a85d75ac32a7d5aa965cf273a5 Mon Sep 17 00:00:00 2001 From: Joerg Steffens Date: Fri, 30 Oct 2020 16:00:05 +0100 Subject: [PATCH 09/10] unittest: change default TLS protocol to ssl.PROTOCOL_TLS Change default TLS protocol from ssl.PROTOCOL_TLSv1_2 to ssl.PROTOCOL_TLS with fallback to ssl.PROTOCOL_SSLv23. On Mac osx stick to ssl.PROTOCOL_TLSv1_2, as ssl.PROTOCOL_TLS and ssl.PROTOCOL_SSLv23 are known to fail in the Travis.org Mac osx test environment. Also skip tests, if the required TLS protocol is not available. --- sslpsk/test/test_sslpsk.py | 56 ++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/sslpsk/test/test_sslpsk.py b/sslpsk/test/test_sslpsk.py index 1f37f85..0ff6921 100644 --- a/sslpsk/test/test_sslpsk.py +++ b/sslpsk/test/test_sslpsk.py @@ -27,10 +27,20 @@ HOST = "localhost" PORT = 6000 -TEST_DATA = b"abcdefghi" CIPHERS = "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH" +TEST_DATA = b"abcdefghi" TIMEOUT = 3 +TLS_PROTOCOL = None +if hasattr(ssl, "PROTOCOL_TLS"): + TLS_PROTOCOL = ssl.PROTOCOL_TLS +else: + TLS_PROTOCOL = ssl.PROTOCOL_SSLv23 +if os.environ.get("TRAVIS_OS_NAME") == "osx": + # the travis Mac osx environment is known to fail + # with protocol ssl.PROTOCOL_TLS. + # Therefore we choose ssl.PROTOCOL_TLSv1_2 + TLS_PROTOCOL = ssl.PROTOCOL_TLSv1_2 def cmd_exists(cmd): return any( @@ -97,7 +107,7 @@ def tearDown(self): self.proc.terminate() self.proc = None - def startServer(self, ssl_version=ssl.PROTOCOL_TLSv1_2, ciphers=CIPHERS, myid=None): + def startServer(self, ssl_version=TLS_PROTOCOL, ciphers=CIPHERS, myid=None): self.accept_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.accept_socket.bind(self.addr) self.accept_socket.listen(1) @@ -151,7 +161,7 @@ def accept(): self.server_thread.start() def connectAndReceiveData( - self, ssl_version=ssl.PROTOCOL_TLSv1_2, ciphers=CIPHERS, myid=None + self, ssl_version=TLS_PROTOCOL, ciphers=CIPHERS, myid=None ): # initialize self.client_socket.connect(self.addr) @@ -178,15 +188,13 @@ def testClient(self): def testCiphersPskAes256(self): ciphers = "PSK-AES256-CBC-SHA" - ssl_version = ssl.PROTOCOL_TLSv1_2 - self.startServer(ssl_version, ciphers) - self.connectAndReceiveData(ssl_version, ciphers) + self.startServer(ciphers=ciphers) + self.connectAndReceiveData(ciphers=ciphers) def testCiphersAll(self): ciphers = "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH" - ssl_version = ssl.PROTOCOL_TLSv1_2 - self.startServer(ssl_version, ciphers) - self.connectAndReceiveData(ssl_version, ciphers) + self.startServer(ciphers=ciphers) + self.connectAndReceiveData(ciphers=ciphers) @unittest.skipUnless( hasattr(ssl, "PROTOCOL_TLS"), "ssl module does not provide required protocol" @@ -199,16 +207,25 @@ def testProtocolTls(self): self.startServer(ssl_version=ssl_version) self.connectAndReceiveData(ssl_version=ssl_version) + @unittest.skipUnless( + hasattr(ssl, "PROTOCOL_TLSv1"), "ssl module does not provide required protocol" + ) def testProtocolTlsV1(self): ssl_version = ssl.PROTOCOL_TLSv1 self.startServer(ssl_version=ssl_version) self.connectAndReceiveData(ssl_version=ssl_version) + @unittest.skipUnless( + hasattr(ssl, "PROTOCOL_TLSv1_1"), "ssl module does not provide required protocol" + ) def testProtocolTlsV1_1(self): ssl_version = ssl.PROTOCOL_TLSv1_1 self.startServer(ssl_version=ssl_version) self.connectAndReceiveData(ssl_version=ssl_version) + @unittest.skipUnless( + hasattr(ssl, "PROTOCOL_TLSv1_2"), "ssl module does not provide required protocol" + ) def testProtocolTlsV1_2(self): ssl_version = ssl.PROTOCOL_TLSv1_2 self.startServer(ssl_version=ssl_version) @@ -217,6 +234,9 @@ def testProtocolTlsV1_2(self): @unittest.skipUnless( hasattr(ssl, "PROTOCOL_TLS"), "ssl module does not provide required protocol" ) + @unittest.skipUnless( + hasattr(ssl, "PROTOCOL_TLSv1_2"), "ssl module does not provide required protocol" + ) def testProtocolClientTlsV1_2ServerTls(self): self.startServer(ssl_version=ssl.PROTOCOL_TLS) self.connectAndReceiveData(ssl_version=ssl.PROTOCOL_TLSv1_2) @@ -322,12 +342,21 @@ def connectOpenSslClientWithSslPskServer( def testProtocolTls(self): self.connectOpenSslClientWithSslPskServer(ssl_version=ssl.PROTOCOL_TLS) + @unittest.skipUnless( + hasattr(ssl, "PROTOCOL_TLSv1"), "ssl module does not provide required protocol" + ) def testProtocolTlsV1(self): self.connectOpenSslClientWithSslPskServer(ssl_version=ssl.PROTOCOL_TLSv1) + @unittest.skipUnless( + hasattr(ssl, "PROTOCOL_TLSv1_1"), "ssl module does not provide required protocol" + ) def testProtocolTlsV1_1(self): self.connectOpenSslClientWithSslPskServer(ssl_version=ssl.PROTOCOL_TLSv1_1) + @unittest.skipUnless( + hasattr(ssl, "PROTOCOL_TLSv1_2"), "ssl module does not provide required protocol" + ) def testProtocolTlsV1_2(self): self.connectOpenSslClientWithSslPskServer(ssl_version=ssl.PROTOCOL_TLSv1_2) @@ -419,12 +448,21 @@ def connectSshPskClientWithOpenSslServer( def testProtocolTls(self): self.connectSshPskClientWithOpenSslServer(ssl_version=ssl.PROTOCOL_TLS) + @unittest.skipUnless( + hasattr(ssl, "PROTOCOL_TLSv1"), "ssl module does not provide required protocol" + ) def testProtocolTlsV1(self): self.connectSshPskClientWithOpenSslServer(ssl_version=ssl.PROTOCOL_TLSv1) + @unittest.skipUnless( + hasattr(ssl, "PROTOCOL_TLSv1_1"), "ssl module does not provide required protocol" + ) def testProtocolTlsV1_1(self): self.connectSshPskClientWithOpenSslServer(ssl_version=ssl.PROTOCOL_TLSv1_1) + @unittest.skipUnless( + hasattr(ssl, "PROTOCOL_TLSv1_2"), "ssl module does not provide required protocol" + ) def testProtocolTlsV1_2(self): self.connectSshPskClientWithOpenSslServer(ssl_version=ssl.PROTOCOL_TLSv1_2) From 86923debe4f32e2a28c3a32a66b15b96fe64cb5f Mon Sep 17 00:00:00 2001 From: Joerg Steffens Date: Fri, 30 Oct 2020 16:32:51 +0100 Subject: [PATCH 10/10] unittest: add test for ssl.PROTOCOL_SSLv23 This protocol version is deprecated since Python >= 3.6. Instead ssl.PROTOCOL_TLS should be used. However, some platforms in our test environment are to old to support ssl.PROTOCOL_TLS. --- sslpsk/test/test_sslpsk.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sslpsk/test/test_sslpsk.py b/sslpsk/test/test_sslpsk.py index 0ff6921..934cba9 100644 --- a/sslpsk/test/test_sslpsk.py +++ b/sslpsk/test/test_sslpsk.py @@ -196,6 +196,18 @@ def testCiphersAll(self): self.startServer(ciphers=ciphers) self.connectAndReceiveData(ciphers=ciphers) + @unittest.skipUnless( + hasattr(ssl, "PROTOCOL_SSLv23"), "ssl module does not provide required protocol" + ) + @unittest.skipIf( + os.environ.get("TRAVIS_OS_NAME") == "osx", "Mac OS is known to fail" + ) + def testProtocolSslV23(self): + ssl_version = ssl.PROTOCOL_SSLv23 + self.startServer(ssl_version=ssl_version) + self.connectAndReceiveData(ssl_version=ssl_version) + + @unittest.skipUnless( hasattr(ssl, "PROTOCOL_TLS"), "ssl module does not provide required protocol" )