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()