github.com/Psiphon-Labs/tls-tris@v0.0.0-20230824155421-58bf6d336a9a/_dev/interop_test_runner (about)

     1  #!/usr/bin/env python2
     2  
     3  import docker
     4  import unittest
     5  import re
     6  import time
     7  
     8  # Regex patterns used for testing
     9  
    10  # Checks if TLS 1.3 was negotiated
    11  RE_PATTERN_HELLO_TLS_13_NORESUME = "^.*Hello TLS 1.3 \(draft .*\) _o/$|^.*Hello TLS 1.3 _o/$"
    12  # Checks if TLS 1.3 was resumed
    13  RE_PATTERN_HELLO_TLS_13_RESUME   = "Hello TLS 1.3 \[resumed\] _o/"
    14  # Checks if 0-RTT was used and NOT confirmed
    15  RE_PATTERN_HELLO_0RTT            = "^.*Hello TLS 1.3 .*\[resumed\] \[0-RTT\] _o/$"
    16  # Checks if 0-RTT was used and confirmed
    17  RE_PATTERN_HELLO_0RTT_CONFIRMED  = "^.*Hello TLS 1.3 .*\[resumed\] \[0-RTT confirmed\] _o/$"
    18  # ALPN
    19  RE_PATTERN_ALPN = "ALPN protocol: npn_proto$"
    20  
    21  class Docker(object):
    22      ''' Utility class used for starting/stoping servers and clients during tests'''
    23      def __init__(self):
    24          self.d = docker.from_env()
    25  
    26      def get_ip(self, server):
    27          tris_localserver_container = self.d.containers.get(server)
    28          return tris_localserver_container.attrs['NetworkSettings']['IPAddress']
    29  
    30      def run_client(self, image_name, cmd):
    31          ''' Runs client and returns tuple (status_code, logs) '''
    32          c = self.d.containers.create(image=image_name, command=cmd)
    33          c.start()
    34          res = c.wait()
    35          ret = c.logs()
    36          c.remove()
    37          return (res['StatusCode'], ret)
    38  
    39      def run_server(self, image_name, cmd=None, ports=None, entrypoint=None):
    40          ''' Starts server and returns docker container '''
    41          c = self.d.containers.create(image=image_name, detach=True, command=cmd, ports=ports, entrypoint=entrypoint)
    42          c.start()
    43          # TODO: maybe can be done better?
    44          time.sleep(3)
    45          return c
    46  
    47  class RegexSelfTest(unittest.TestCase):
    48      ''' Ensures that those regexe's actually work '''
    49  
    50      LINE_HELLO_TLS      ="\nsomestuff\nHello TLS 1.3 _o/\nsomestuff"
    51      LINE_HELLO_DRAFT_TLS="\nsomestuff\nHello TLS 1.3 (draft 23) _o/\nsomestuff"
    52  
    53      LINE_HELLO_RESUMED  ="\nsomestuff\nHello TLS 1.3 [resumed] _o/\nsomestuff"
    54      LINE_HELLO_MIXED    ="\nsomestuff\nHello TLS 1.3 (draft 23) _o/\nHello TLS 1.3 (draft 23) [resumed] _o/\nsomestuff"
    55      LINE_HELLO_TLS_12   ="\nsomestuff\nHello TLS 1.2 (draft 23) [resumed] _o/\nsomestuff"
    56      LINE_HELLO_TLS_13_0RTT="\nsomestuff\nHello TLS 1.3 (draft 23) [resumed] [0-RTT] _o/\nsomestuff"
    57      LINE_HELLO_TLS_13_0RTT_CONFIRMED="\nsomestuff\nHello TLS 1.3 (draft 23) [resumed] [0-RTT confirmed] _o/\nsomestuff"
    58  
    59      def test_regexes(self):
    60          self.assertIsNotNone(
    61              re.search(RE_PATTERN_HELLO_TLS_13_NORESUME, RegexSelfTest.LINE_HELLO_TLS, re.MULTILINE))
    62          self.assertIsNotNone(
    63              re.search(RE_PATTERN_HELLO_TLS_13_NORESUME, RegexSelfTest.LINE_HELLO_DRAFT_TLS, re.MULTILINE))
    64          self.assertIsNotNone(
    65              re.search(RE_PATTERN_HELLO_TLS_13_RESUME, RegexSelfTest.LINE_HELLO_RESUMED, re.MULTILINE))
    66          self.assertIsNotNone(
    67              re.search(RE_PATTERN_HELLO_0RTT, RegexSelfTest.LINE_HELLO_TLS_13_0RTT, re.MULTILINE))
    68          self.assertIsNotNone(
    69              re.search(RE_PATTERN_HELLO_0RTT_CONFIRMED, RegexSelfTest.LINE_HELLO_TLS_13_0RTT_CONFIRMED, re.MULTILINE))
    70  
    71          # negative cases
    72  
    73          # expects 1.3, but 1.2 received
    74          self.assertIsNone(
    75              re.search(RE_PATTERN_HELLO_TLS_13_NORESUME, RegexSelfTest.LINE_HELLO_TLS_12, re.MULTILINE))
    76          # expects 0-RTT
    77          self.assertIsNone(
    78              re.search(RE_PATTERN_HELLO_TLS_13_RESUME, RegexSelfTest.LINE_HELLO_TLS_13_0RTT, re.MULTILINE))
    79          # expectes 0-RTT confirmed
    80          self.assertIsNone(
    81              re.search(RE_PATTERN_HELLO_0RTT, RegexSelfTest.LINE_HELLO_TLS_13_0RTT_CONFIRMED, re.MULTILINE))
    82          # expects resume without 0-RTT
    83          self.assertIsNone(
    84              re.search(RE_PATTERN_HELLO_TLS_13_RESUME, RegexSelfTest.LINE_HELLO_TLS_13_0RTT, re.MULTILINE))
    85  
    86  
    87  class InteropServer(object):
    88      ''' Instantiates TRIS as a server '''
    89  
    90      TRIS_SERVER_NAME = "tris-localserver"
    91  
    92      @classmethod
    93      def setUpClass(self):
    94          self.d = Docker()
    95          self.server = self.d.run_server(self.TRIS_SERVER_NAME)
    96  
    97      @classmethod
    98      def tearDownClass(self):
    99          self.server.kill()
   100          self.server.remove()
   101  
   102      @property
   103      def server_ip(self):
   104          return self.d.get_ip(self.server.name)
   105  
   106  # Mixins for testing server functionality
   107  
   108  class ServerNominalMixin(object):
   109      ''' Nominal tests for TLS 1.3 - client tries to perform handshake with server '''
   110      def test_rsa(self):
   111          res = self.d.run_client(self.CLIENT_NAME, self.server_ip+":"+'1443')
   112          self.assertTrue(res[0] == 0)
   113          # Check there was TLS hello without resume
   114          self.assertIsNotNone(
   115              re.search(RE_PATTERN_HELLO_TLS_13_NORESUME, res[1], re.MULTILINE))
   116          # Check there was TLS hello with resume
   117          self.assertIsNotNone(
   118              re.search(RE_PATTERN_HELLO_TLS_13_RESUME, res[1], re.MULTILINE))
   119  
   120      def test_ecdsa(self):
   121          res = self.d.run_client(self.CLIENT_NAME, self.server_ip+":"+'2443')
   122          self.assertTrue(res[0] == 0)
   123          # Check there was TLS hello without resume
   124          self.assertIsNotNone(
   125              re.search(RE_PATTERN_HELLO_TLS_13_NORESUME, res[1], re.MULTILINE))
   126          # Check there was TLS hello with resume
   127          self.assertIsNotNone(
   128              re.search(RE_PATTERN_HELLO_TLS_13_RESUME, res[1], re.MULTILINE))
   129  
   130  class ServerClientAuthMixin(object):
   131      ''' Client authentication testing '''
   132      def test_client_auth(self):
   133          args = ''.join([self.server_ip+':6443',' -key client_rsa.key -cert client_rsa.crt -debug'])
   134          res = self.d.run_client(self.CLIENT_NAME, args)
   135          self.assertEqual(res[0], 0)
   136          # Check there was TLS hello without resume
   137          self.assertIsNotNone(
   138              re.search(RE_PATTERN_HELLO_TLS_13_NORESUME, res[1], re.MULTILINE))
   139          # Check there was TLS hello with resume
   140          self.assertIsNotNone(
   141              re.search(RE_PATTERN_HELLO_TLS_13_RESUME, res[1], re.MULTILINE))
   142  
   143  class ClientNominalMixin(object):
   144  
   145      def test_rsa(self):
   146          res = self.d.run_client(self.CLIENT_NAME, '-ecdsa=false '+self.server_ip+":1443")
   147          self.assertEqual(res[0], 0)
   148  
   149      def test_ecdsa(self):
   150          res = self.d.run_client(self.CLIENT_NAME, '-rsa=false '+self.server_ip+":2443")
   151          self.assertEqual(res[0], 0)
   152  
   153  
   154  class ClientClientAuthMixin(object):
   155      ''' Client authentication testing - tris on client side '''
   156  
   157      def test_client_auth(self):
   158          res = self.d.run_client('tris-testclient', '-rsa=false -cliauth '+self.server_ip+":6443")
   159          self.assertTrue(res[0] == 0)
   160  
   161  class ServerZeroRttMixin(object):
   162      ''' Zero RTT testing '''
   163  
   164      def test_zero_rtt(self):
   165          # rejecting 0-RTT
   166          res = self.d.run_client(self.CLIENT_NAME, self.server_ip+":3443")
   167          self.assertEqual(res[0], 0)
   168          self.assertIsNotNone(
   169              re.search(RE_PATTERN_HELLO_TLS_13_RESUME, res[1], re.MULTILINE))
   170          self.assertIsNone(
   171              re.search(RE_PATTERN_HELLO_0RTT, res[1], re.MULTILINE))
   172  
   173          # accepting 0-RTT
   174          res = self.d.run_client(self.CLIENT_NAME, self.server_ip+":4443")
   175          self.assertEqual(res[0], 0)
   176          self.assertIsNotNone(
   177              re.search(RE_PATTERN_HELLO_0RTT, res[1], re.MULTILINE))
   178  
   179          # confirming 0-RTT
   180          res = self.d.run_client(self.CLIENT_NAME, self.server_ip+":5443")
   181          self.assertEqual(res[0], 0)
   182          self.assertIsNotNone(
   183              re.search(RE_PATTERN_HELLO_0RTT_CONFIRMED, res[1], re.MULTILINE))
   184  
   185  class InteropClient(object):
   186      ''' Instantiates TRIS as a client '''
   187  
   188      CLIENT_NAME = "tris-testclient"
   189  
   190      @classmethod
   191      def setUpClass(self):
   192          self.d = Docker()
   193          self.server = self.d.run_server(
   194                              self.SERVER_NAME,
   195                              ports={ '1443/tcp': 1443, '2443/tcp': 2443, '6443/tcp': 6443},
   196                              entrypoint="/server.sh")
   197  
   198      @classmethod
   199      def tearDownClass(self):
   200          self.server.kill()
   201          self.server.remove()
   202  
   203      @property
   204      def server_ip(self):
   205          return self.d.get_ip(self.server.name)
   206  
   207  # Actual test definition
   208  
   209  # TRIS as a server
   210  class InteropServer_BoringSSL(InteropServer, ServerNominalMixin, ServerClientAuthMixin, unittest.TestCase):
   211  
   212      CLIENT_NAME = "tls-tris:boring"
   213  
   214      def test_ALPN(self):
   215          '''
   216          Checks wether ALPN is sent back by tris server in EncryptedExtensions in case of TLS 1.3. The
   217          ALPN protocol is set to 'npn_proto', which is hardcoded in TRIS test server.
   218          '''
   219          res = self.d.run_client(self.CLIENT_NAME, self.server_ip+":1443 "+'-alpn-protos npn_proto -debug')
   220          print(res[1])
   221          self.assertEqual(res[0], 0)
   222          self.assertIsNotNone(re.search(RE_PATTERN_ALPN, res[1], re.MULTILINE))
   223  
   224  
   225  # PicoTLS doesn't seem to implement draft-23 correctly. It will
   226  # be enabled when draft-28 is implemented.
   227  # class InteropServer_PicoTLS(
   228  #         InteropServer,
   229  #         ServerNominalMixin,
   230  #         ServerZeroRttMixin,
   231  #         unittest.TestCase
   232  #     ): CLIENT_NAME = "tls-tris:picotls"
   233  
   234  class InteropServer_NSS(
   235          InteropServer,
   236          ServerNominalMixin,
   237          ServerZeroRttMixin,
   238          unittest.TestCase
   239      ): CLIENT_NAME = "tls-tris:tstclnt"
   240  
   241  # TRIS as a client
   242  class InteropClient_BoringSSL(
   243          InteropClient,
   244          ClientNominalMixin,
   245          ClientClientAuthMixin,
   246          unittest.TestCase
   247      ): SERVER_NAME = "boring-localserver"
   248  
   249  class InteropClient_NSS(
   250          InteropClient,
   251          ClientNominalMixin,
   252          unittest.TestCase
   253      ): SERVER_NAME = "tstclnt-localserver"
   254  
   255  # TRIS as a client
   256  class InteropServer_TRIS(ClientNominalMixin, InteropServer, unittest.TestCase):
   257  
   258      CLIENT_NAME = 'tris-testclient'
   259  
   260      def test_client_auth(self):
   261          # I need to block TLS v1.2 as test server needs some rework
   262          res = self.d.run_client(self.CLIENT_NAME, '-rsa=false -ecdsa=false -cliauth '+self.server_ip+":6443")
   263          self.assertEqual(res[0], 0)
   264  
   265  if __name__ == '__main__':
   266      unittest.main()