Kris Kwiatkowski
76231e7564
Tris tries to connect to BoringSSL over TLS 1.2 with X25519-SIDH as prefered DH group. As this is not supported by BoringSSL it must fall back to P-256 (second preference on the list) Also refactors tris test client
305 lines
12 KiB
Python
Executable File
305 lines
12 KiB
Python
Executable File
#!/usr/bin/env python2
|
|
|
|
import docker
|
|
import unittest
|
|
import re
|
|
import time
|
|
|
|
# Regex patterns used for testing
|
|
|
|
# Checks if TLS 1.3 was negotiated
|
|
RE_PATTERN_HELLO_TLS_13_NORESUME = "^.*Hello TLS 1.3 \(draft .*\) _o/$|^.*Hello TLS 1.3 _o/$"
|
|
# Checks if TLS 1.3 was resumed
|
|
RE_PATTERN_HELLO_TLS_13_RESUME = "Hello TLS 1.3 \[resumed\] _o/"
|
|
# Checks if 0-RTT was used and NOT confirmed
|
|
RE_PATTERN_HELLO_0RTT = "^.*Hello TLS 1.3 .*\[resumed\] \[0-RTT\] _o/$"
|
|
# Checks if 0-RTT was used and confirmed
|
|
RE_PATTERN_HELLO_0RTT_CONFIRMED = "^.*Hello TLS 1.3 .*\[resumed\] \[0-RTT confirmed\] _o/$"
|
|
# ALPN
|
|
RE_PATTERN_ALPN = "ALPN protocol: npn_proto$"
|
|
# Successful TLS establishement from TRIS
|
|
RE_TRIS_ALL_PASSED = ".*All handshakes passed.*"
|
|
# TLS handshake from BoringSSL with SIDH/P503-X25519
|
|
RE_BORINGSSL_P503 = "ECDHE curve: X25519-SIDHp503"
|
|
|
|
class Docker(object):
|
|
''' Utility class used for starting/stoping servers and clients during tests'''
|
|
def __init__(self):
|
|
self.d = docker.from_env()
|
|
|
|
def get_ip(self, server):
|
|
tris_localserver_container = self.d.containers.get(server)
|
|
return tris_localserver_container.attrs['NetworkSettings']['IPAddress']
|
|
|
|
def run_client(self, image_name, cmd):
|
|
''' Runs client and returns tuple (status_code, logs) '''
|
|
c = self.d.containers.create(image=image_name, command=cmd)
|
|
c.start()
|
|
res = c.wait()
|
|
ret = c.logs()
|
|
c.remove()
|
|
return (res['StatusCode'], ret)
|
|
|
|
def run_server(self, image_name, cmd=None, ports=None, entrypoint=None):
|
|
''' Starts server and returns docker container '''
|
|
c = self.d.containers.create(image=image_name, detach=True, command=cmd, ports=ports, entrypoint=entrypoint)
|
|
c.start()
|
|
# TODO: maybe can be done better?
|
|
time.sleep(3)
|
|
return c
|
|
|
|
class RegexSelfTest(unittest.TestCase):
|
|
''' Ensures that those regexe's actually work '''
|
|
|
|
LINE_HELLO_TLS ="\nsomestuff\nHello TLS 1.3 _o/\nsomestuff"
|
|
LINE_HELLO_DRAFT_TLS="\nsomestuff\nHello TLS 1.3 (draft 23) _o/\nsomestuff"
|
|
|
|
LINE_HELLO_RESUMED ="\nsomestuff\nHello TLS 1.3 [resumed] _o/\nsomestuff"
|
|
LINE_HELLO_MIXED ="\nsomestuff\nHello TLS 1.3 (draft 23) _o/\nHello TLS 1.3 (draft 23) [resumed] _o/\nsomestuff"
|
|
LINE_HELLO_TLS_12 ="\nsomestuff\nHello TLS 1.2 (draft 23) [resumed] _o/\nsomestuff"
|
|
LINE_HELLO_TLS_13_0RTT="\nsomestuff\nHello TLS 1.3 (draft 23) [resumed] [0-RTT] _o/\nsomestuff"
|
|
LINE_HELLO_TLS_13_0RTT_CONFIRMED="\nsomestuff\nHello TLS 1.3 (draft 23) [resumed] [0-RTT confirmed] _o/\nsomestuff"
|
|
def test_regexes(self):
|
|
self.assertIsNotNone(
|
|
re.search(RE_PATTERN_HELLO_TLS_13_NORESUME, RegexSelfTest.LINE_HELLO_TLS, re.MULTILINE))
|
|
self.assertIsNotNone(
|
|
re.search(RE_PATTERN_HELLO_TLS_13_NORESUME, RegexSelfTest.LINE_HELLO_DRAFT_TLS, re.MULTILINE))
|
|
self.assertIsNotNone(
|
|
re.search(RE_PATTERN_HELLO_TLS_13_RESUME, RegexSelfTest.LINE_HELLO_RESUMED, re.MULTILINE))
|
|
self.assertIsNotNone(
|
|
re.search(RE_PATTERN_HELLO_0RTT, RegexSelfTest.LINE_HELLO_TLS_13_0RTT, re.MULTILINE))
|
|
self.assertIsNotNone(
|
|
re.search(RE_PATTERN_HELLO_0RTT_CONFIRMED, RegexSelfTest.LINE_HELLO_TLS_13_0RTT_CONFIRMED, re.MULTILINE))
|
|
|
|
# negative cases
|
|
|
|
# expects 1.3, but 1.2 received
|
|
self.assertIsNone(
|
|
re.search(RE_PATTERN_HELLO_TLS_13_NORESUME, RegexSelfTest.LINE_HELLO_TLS_12, re.MULTILINE))
|
|
# expects 0-RTT
|
|
self.assertIsNone(
|
|
re.search(RE_PATTERN_HELLO_TLS_13_RESUME, RegexSelfTest.LINE_HELLO_TLS_13_0RTT, re.MULTILINE))
|
|
# expectes 0-RTT confirmed
|
|
self.assertIsNone(
|
|
re.search(RE_PATTERN_HELLO_0RTT, RegexSelfTest.LINE_HELLO_TLS_13_0RTT_CONFIRMED, re.MULTILINE))
|
|
# expects resume without 0-RTT
|
|
self.assertIsNone(
|
|
re.search(RE_PATTERN_HELLO_TLS_13_RESUME, RegexSelfTest.LINE_HELLO_TLS_13_0RTT, re.MULTILINE))
|
|
|
|
|
|
class InteropServer(object):
|
|
''' Instantiates TRIS as a server '''
|
|
|
|
TRIS_SERVER_NAME = "tris-localserver"
|
|
|
|
@classmethod
|
|
def setUpClass(self):
|
|
self.d = Docker()
|
|
self.server = self.d.run_server(self.TRIS_SERVER_NAME)
|
|
|
|
@classmethod
|
|
def tearDownClass(self):
|
|
self.server.kill()
|
|
self.server.remove()
|
|
|
|
@property
|
|
def server_ip(self):
|
|
return self.d.get_ip(self.server.name)
|
|
|
|
# Mixins for testing server functionality
|
|
|
|
class ServerNominalMixin(object):
|
|
''' Nominal tests for TLS 1.3 - client tries to perform handshake with server '''
|
|
def test_rsa(self):
|
|
res = self.d.run_client(self.CLIENT_NAME, self.server_ip+":"+'1443')
|
|
self.assertTrue(res[0] == 0)
|
|
# Check there was TLS hello without resume
|
|
self.assertIsNotNone(
|
|
re.search(RE_PATTERN_HELLO_TLS_13_NORESUME, res[1], re.MULTILINE))
|
|
# Check there was TLS hello with resume
|
|
self.assertIsNotNone(
|
|
re.search(RE_PATTERN_HELLO_TLS_13_RESUME, res[1], re.MULTILINE))
|
|
|
|
def test_ecdsa(self):
|
|
res = self.d.run_client(self.CLIENT_NAME, self.server_ip+":"+'2443')
|
|
self.assertTrue(res[0] == 0)
|
|
# Check there was TLS hello without resume
|
|
self.assertIsNotNone(
|
|
re.search(RE_PATTERN_HELLO_TLS_13_NORESUME, res[1], re.MULTILINE))
|
|
# Check there was TLS hello with resume
|
|
self.assertIsNotNone(
|
|
re.search(RE_PATTERN_HELLO_TLS_13_RESUME, res[1], re.MULTILINE))
|
|
|
|
class ServerClientAuthMixin(object):
|
|
''' Client authentication testing '''
|
|
def test_client_auth(self):
|
|
args = ''.join([self.server_ip+':6443',' -key client_rsa.key -cert client_rsa.crt -debug'])
|
|
res = self.d.run_client(self.CLIENT_NAME, args)
|
|
self.assertEqual(res[0], 0)
|
|
# Check there was TLS hello without resume
|
|
self.assertIsNotNone(
|
|
re.search(RE_PATTERN_HELLO_TLS_13_NORESUME, res[1], re.MULTILINE))
|
|
# Check there was TLS hello with resume
|
|
self.assertIsNotNone(
|
|
re.search(RE_PATTERN_HELLO_TLS_13_RESUME, res[1], re.MULTILINE))
|
|
|
|
class ClientNominalMixin(object):
|
|
|
|
def test_rsa(self):
|
|
res = self.d.run_client(self.CLIENT_NAME, '-ecdsa=false '+self.server_ip+":1443")
|
|
self.assertEqual(res[0], 0)
|
|
|
|
def test_ecdsa(self):
|
|
res = self.d.run_client(self.CLIENT_NAME, '-rsa=false '+self.server_ip+":2443")
|
|
self.assertEqual(res[0], 0)
|
|
|
|
|
|
class ClientClientAuthMixin(object):
|
|
''' Client authentication testing - tris on client side '''
|
|
|
|
def test_client_auth(self):
|
|
res = self.d.run_client('tris-testclient', '-rsa=false -cliauth '+self.server_ip+":6443")
|
|
self.assertTrue(res[0] == 0)
|
|
|
|
class ServerZeroRttMixin(object):
|
|
''' Zero RTT testing '''
|
|
|
|
def test_zero_rtt(self):
|
|
# rejecting 0-RTT
|
|
res = self.d.run_client(self.CLIENT_NAME, self.server_ip+":3443")
|
|
self.assertEqual(res[0], 0)
|
|
self.assertIsNotNone(
|
|
re.search(RE_PATTERN_HELLO_TLS_13_RESUME, res[1], re.MULTILINE))
|
|
self.assertIsNone(
|
|
re.search(RE_PATTERN_HELLO_0RTT, res[1], re.MULTILINE))
|
|
|
|
# accepting 0-RTT
|
|
res = self.d.run_client(self.CLIENT_NAME, self.server_ip+":4443")
|
|
self.assertEqual(res[0], 0)
|
|
self.assertIsNotNone(
|
|
re.search(RE_PATTERN_HELLO_0RTT, res[1], re.MULTILINE))
|
|
|
|
# confirming 0-RTT
|
|
res = self.d.run_client(self.CLIENT_NAME, self.server_ip+":5443")
|
|
self.assertEqual(res[0], 0)
|
|
self.assertIsNotNone(
|
|
re.search(RE_PATTERN_HELLO_0RTT_CONFIRMED, res[1], re.MULTILINE))
|
|
|
|
class InteropClient(object):
|
|
''' Instantiates TRIS as a client '''
|
|
|
|
CLIENT_NAME = "tris-testclient"
|
|
|
|
@classmethod
|
|
def setUpClass(self):
|
|
self.d = Docker()
|
|
self.server = self.d.run_server(
|
|
self.SERVER_NAME,
|
|
ports={ '1443/tcp': 1443, '2443/tcp': 2443, '6443/tcp': 6443, '7443/tcp': 7443},
|
|
entrypoint="/server.sh")
|
|
|
|
@classmethod
|
|
def tearDownClass(self):
|
|
self.server.kill()
|
|
self.server.remove()
|
|
|
|
@property
|
|
def server_ip(self):
|
|
return self.d.get_ip(self.server.name)
|
|
|
|
# Actual test definition
|
|
|
|
# TRIS as a server, BoringSSL as a client
|
|
class InteropServer_BoringSSL(InteropServer, ServerNominalMixin, ServerClientAuthMixin, unittest.TestCase):
|
|
|
|
CLIENT_NAME = "tls-tris:boring"
|
|
|
|
def test_ALPN(self):
|
|
'''
|
|
Checks wether ALPN is sent back by tris server in EncryptedExtensions in case of TLS 1.3. The
|
|
ALPN protocol is set to 'npn_proto', which is hardcoded in TRIS test server.
|
|
'''
|
|
res = self.d.run_client(self.CLIENT_NAME, self.server_ip+":1443 "+'-alpn-protos npn_proto')
|
|
self.assertEqual(res[0], 0)
|
|
self.assertIsNotNone(re.search(RE_PATTERN_ALPN, res[1], re.MULTILINE))
|
|
|
|
def test_SIDH(self):
|
|
'''
|
|
Connects to TRIS server listening on 7443 and tries to perform key agreement with SIDH/P503-X25519
|
|
'''
|
|
res = self.d.run_client(self.CLIENT_NAME, self.server_ip+":7443 "+'-curves X25519-SIDHp503')
|
|
self.assertEqual(res[0], 0)
|
|
self.assertIsNotNone(re.search(RE_BORINGSSL_P503, res[1], re.MULTILINE))
|
|
self.assertIsNotNone(re.search(RE_PATTERN_HELLO_TLS_13_NORESUME, res[1], re.MULTILINE))
|
|
|
|
# PicoTLS doesn't seem to implement draft-23 correctly. It will
|
|
# be enabled when draft-28 is implemented.
|
|
# class InteropServer_PicoTLS(
|
|
# InteropServer,
|
|
# ServerNominalMixin,
|
|
# ServerZeroRttMixin,
|
|
# unittest.TestCase
|
|
# ): CLIENT_NAME = "tls-tris:picotls"
|
|
|
|
class InteropServer_NSS(
|
|
InteropServer,
|
|
ServerNominalMixin,
|
|
ServerZeroRttMixin,
|
|
unittest.TestCase
|
|
): CLIENT_NAME = "tls-tris:tstclnt"
|
|
|
|
# TRIS as a client, BoringSSL as a server
|
|
class InteropClient_BoringSSL(InteropClient, ClientNominalMixin, ClientClientAuthMixin, unittest.TestCase):
|
|
|
|
SERVER_NAME = "boring-localserver"
|
|
|
|
def test_SIDH(self):
|
|
'''
|
|
Connects to BoringSSL server listening on 7443 and tries to perform key agreement with SIDH/P503-X25519
|
|
'''
|
|
res = self.d.run_client(self.CLIENT_NAME, '-rsa=false -ecdsa=true -groups X25519-SIDHp503 ' + self.server_ip+":7443")
|
|
self.assertEqual(res[0], 0)
|
|
self.assertIsNotNone(re.search(RE_TRIS_ALL_PASSED, res[1], re.MULTILINE))
|
|
|
|
def test_SIDH_TLSv12(self):
|
|
'''
|
|
Connects to TRIS server listening on 7443 and tries to perform key agreement with SIDH/P503-X25519
|
|
This connection will be over TLSv12 and hence it should fall back to X25519
|
|
'''
|
|
res = self.d.run_client(self.CLIENT_NAME, '-tls_version=1.2 -rsa=false -ecdsa=true -groups X25519-SIDHp503:P-256 -ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 ' + self.server_ip+":7443")
|
|
self.assertEqual(res[0], 0)
|
|
# Go doesn't provide API to get NamedGroup ID, but boringssl on port 7443 accepts only TLS1.2
|
|
# so if handshake was successful, then that's all what we need
|
|
self.assertIsNotNone(re.search(RE_TRIS_ALL_PASSED, res[1], re.MULTILINE))
|
|
|
|
class InteropClient_NSS(
|
|
InteropClient,
|
|
ClientNominalMixin,
|
|
unittest.TestCase
|
|
): SERVER_NAME = "tstclnt-localserver"
|
|
|
|
# TRIS as a client
|
|
class InteropServer_TRIS(ClientNominalMixin, InteropServer, unittest.TestCase):
|
|
|
|
CLIENT_NAME = 'tris-testclient'
|
|
|
|
def test_client_auth(self):
|
|
# I need to block TLS v1.2 as test server needs some rework
|
|
res = self.d.run_client(self.CLIENT_NAME, '-rsa=false -ecdsa=false -cliauth '+self.server_ip+":6443")
|
|
self.assertEqual(res[0], 0)
|
|
|
|
def test_SIDH(self):
|
|
res = self.d.run_client(self.CLIENT_NAME, '-rsa=false -ecdsa=true -groups X25519-SIDHp503 '+self.server_ip+":7443")
|
|
self.assertEqual(res[0], 0)
|
|
|
|
def test_server_doesnt_support_SIDH(self):
|
|
'''
|
|
Client advertises HybridSIDH and ECDH. Server supports ECDH only. Checks weather
|
|
TLS session can still be established.
|
|
'''
|
|
res = self.d.run_client(self.CLIENT_NAME, '-rsa=false -ecdsa=true '+self.server_ip+":7443")
|
|
self.assertEqual(res[0], 0)
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|