Skip to content

Initial TLS 1.2 handshake test #103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jul 31, 2019
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1eea6da
Initial TLS 1.2 handshake test
krizhanovsky Jun 12, 2019
7f14f2e
Use smaller receive timeout for the test suite
krizhanovsky Jun 19, 2019
1734ecf
Make pylint happier
krizhanovsky Jun 20, 2019
c49388f
that the connections are correctly terminated.
krizhanovsky Jun 20, 2019
38708b9
Preliminary deproxy ssl support. The code doesn't work, but
krizhanovsky Jun 27, 2019
351a4e2
Use the new framework for tls_stress test
krizhanovsky Jun 27, 2019
519f4d8
Also introduces TLS mode for Deproxy.
krizhanovsky Jun 29, 2019
fb2d97c
The x509 certificate generator is placed in /framework and
krizhanovsky Jul 3, 2019
621cbd5
Tests for per-vhost certificates and SNI handling:
krizhanovsky Jul 14, 2019
c16b8eb
Test extensions from RFC 6066, invalid extensions and too long extens…
krizhanovsky Jul 15, 2019
43ad7cc
Fixes for tests for #715 by @ikoveshnikov
krizhanovsky Jul 17, 2019
3356b75
Fix bugs in TLS handle_read() and config parsing.
krizhanovsky Jul 18, 2019
840ac46
Add tests for default SNI handling versus tls_fallback_default.
krizhanovsky Jul 18, 2019
58786e1
Replace example.com by tempesta-tech.com
krizhanovsky Jul 18, 2019
14754d2
Basic TLS test for wrong HTTP request
krizhanovsky Jul 20, 2019
2424aab
Test for bad renegotiation info (RFC 5746 3.6)
krizhanovsky Jul 21, 2019
4dd72f6
Fuzzing test and test for alerts
krizhanovsky Jul 22, 2019
0758f01
Chunked TLS transfers. Fix small bug.
krizhanovsky Jul 24, 2019
61430eb
TCP segmentation test for TLS
krizhanovsky Jul 25, 2019
8e9e790
Using Exception considered as a bad practice, pylint also isn't happy
krizhanovsky Jul 25, 2019
2fc93d0
Test case for Connection: close (issue #1326)
krizhanovsky Jul 26, 2019
0d18c6b
Several fixes; disable several tests; adopt to #715
krizhanovsky Jul 28, 2019
f3d58d9
Sevaral small fixes and cleanups
krizhanovsky Jul 30, 2019
f99a725
Start Tempesta synchronously and wait for appropriate message in dmesg
krizhanovsky Jul 30, 2019
e558680
Unlimit net ratelimiter to have dmesg records for tests
krizhanovsky Jul 30, 2019
e4e7fb3
Multiple fixes in TLS tests and the infrastructure
krizhanovsky Jul 31, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ deproxy server, and workload tests should use wrk client and nginx server.
## Requirements

- Host for testing framework: `Python2`, `python2-paramiko`,
`python-configparser`, `python-subprocess32`, `wrk`, `ab`, `python-scapy`
`python-configparser`, `python-subprocess32`, `wrk`, `ab`, `python-scapy`,
`python-cryptography`
- All hosts except previous one: `sftp-server`
- Host for running TempestaFW: Linux kernel with Tempesta, TempestaFW sources,
`systemtap`, `tcpdump`, `bc`
Expand Down Expand Up @@ -123,6 +124,9 @@ is `10` seconds.

`log_file` option specifies a file to tee (duplicate) tests' stderr to.

`workdir` - path to temporary files, e.g. TLS certificates generated by
the framework.

This group of options can be overridden by command line options, for more
information run tests with `-h` key.
```sh
Expand Down Expand Up @@ -157,6 +161,7 @@ SSH. If hostname is `localhost`, TempestaFW will be ran locally.
`config` — workdir-relative or absolute path to the temporary TempestaFW config
that will be created during testing.


#### Server Section

`ip` — IPv4/IPv6 address of the backend server host in test network, as
Expand Down Expand Up @@ -371,4 +376,12 @@ this, but it simpler to check free ports before start server.
#### Classes for servers and clients

deproxyclient, deproxyserver, nginx, wrk - this classes used for creating
and handling corresponding types of items.
and handling corresponding types of items.


## Resources

There are not so much good references about best practices in development of
testing framework.

* [Why Good Developers Write Bad Tests](https://www.youtube.com/watch?v=oO-FMAdjY68)
3 changes: 2 additions & 1 deletion framework/deproxy_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def handle_read(self):
method=method)
self.response_buffer = \
self.response_buffer[response.original_length:]
except deproxy.IncompliteMessage:
except deproxy.IncompleteMessage:
return
except deproxy.ParseError:
tf_cfg.dbg(4, ('Deproxy: Client: Can\'t parse message\n'
Expand Down Expand Up @@ -113,6 +113,7 @@ def make_request(self, request):
def receive_response(self, response):
raise NotImplementedError("Not implemented 'receive_response()'")


class DeproxyClient(BaseDeproxyClient):
last_response = None
responses = []
Expand Down
10 changes: 9 additions & 1 deletion framework/deproxy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ def __init__(self, server, sock=None, keep_alive=None):
self.request_buffer = ''
tf_cfg.dbg(6, '\tDeproxy: SrvConnection: New server connection.')

def initiate_send(self):
""" Override dispatcher_with_send.initiate_send() which transfers
data with too small chunks of 512 bytes.
"""
num_sent = 0
num_sent = asyncore.dispatcher.send(self, self.out_buffer[:4096])
self.out_buffer = self.out_buffer[num_sent:]

def send_pending_and_close(self):
while len(self.out_buffer):
self.initiate_send()
Expand Down Expand Up @@ -59,7 +67,7 @@ def handle_read(self):
self.request_buffer += self.recv(deproxy.MAX_MESSAGE_SIZE)
try:
request = deproxy.Request(self.request_buffer)
except deproxy.IncompliteMessage:
except deproxy.IncompleteMessage:
return
except deproxy.ParseError:
tf_cfg.dbg(4, ('Deproxy: SrvConnection: Can\'t parse message\n'
Expand Down
34 changes: 22 additions & 12 deletions framework/tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from framework.templates import fill_template, populate_properties

__author__ = 'Tempesta Technologies, Inc.'
__copyright__ = 'Copyright (C) 2018 Tempesta Technologies, Inc.'
__copyright__ = 'Copyright (C) 2018-2019 Tempesta Technologies, Inc.'
__license__ = 'GPL2'

backend_defs = {}
Expand Down Expand Up @@ -61,25 +61,29 @@ def __init__(self, *args, **kwargs):
self.__tempesta = None
self.deproxy_manager = deproxy_manager.DeproxyManager()

def __create_client_deproxy(self, client):
def __create_client_deproxy(self, client, ssl):
addr = fill_template(client['addr'], client)
port = int(fill_template(client['port'], client))
clt = deproxy_client.DeproxyClient(addr=addr, port=port)
clt = deproxy_client.DeproxyClient(addr=addr, port=port, ssl=ssl)
if ssl:
server_hostname = fill_template(client['ssl_hostname'], client)
clt.set_server_hostname(server_hostname)
return clt

def __create_client_wrk(self, client):
def __create_client_wrk(self, client, ssl):
addr = fill_template(client['addr'], client)
wrk = wrk_client.Wrk(server_addr=addr)
wrk = wrk_client.Wrk(server_addr=addr, ssl=ssl)
wrk.set_script(client['id']+"_script", content="")
return wrk

def __create_client(self, client):
populate_properties(client)
ssl = client.setdefault('ssl', False)
cid = client['id']
if client['type'] == 'deproxy':
self.__clients[cid] = self.__create_client_deproxy(client)
self.__clients[cid] = self.__create_client_deproxy(client, ssl)
elif client['type'] == 'wrk':
self.__clients[cid] = self.__create_client_wrk(client)
self.__clients[cid] = self.__create_client_wrk(client, ssl)

def __create_backend(self, server):
srv = None
Expand Down Expand Up @@ -143,6 +147,9 @@ def get_tempesta(self):
def __create_tempesta(self):
desc = self.tempesta.copy()
populate_properties(desc)
custom_cert = False
if 'custom_cert' in desc:
custom_cert = self.tempesta['custom_cert']
config = ""
if 'config' in desc:
config = desc['config']
Expand All @@ -151,7 +158,8 @@ def __create_tempesta(self):
self.__tempesta = factory(desc)
else:
self.__tempesta = default_tempesta_factory(desc)
self.__tempesta.config.set_defconfig(fill_template(config, desc))
self.__tempesta.config.set_defconfig(fill_template(config, desc),
custom_cert)

def start_all_servers(self):
for sid in self.__servers:
Expand All @@ -161,9 +169,11 @@ def start_all_servers(self):
raise Exception("Can not start server %s" % sid)

def start_tempesta(self):
self.__tempesta.start()
if not self.__tempesta.is_running():
raise Exception("Can not start Tempesta")
""" Start Tempesta and wait until the initialization process finish. """
with dmesg.wait_for_msg('[tempesta fw] modules are started', 1, True):
self.__tempesta.start()
if not self.__tempesta.is_running():
raise Exception("Can not start Tempesta")

def start_all_clients(self):
for cid in self.__clients:
Expand Down Expand Up @@ -197,7 +207,7 @@ def tearDown(self):
try:
deproxy_manager.finish_all_deproxy()
except:
print ('Unknown exception in stopping deproxy')
print('Unknown exception in stopping deproxy')

self.oops.update()
if self.oops.warn_count("Oops") > 0:
Expand Down
139 changes: 139 additions & 0 deletions framework/x509.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""
X509 certificates generation required to test handling of different cipher
suites anomalies on Tempesta TLS side. We didn't modify mbedTLS x509 related
code, so now we're not interested with x509 parsing at all, so we do not play
with different x509 formats, extensions and so on.

Without loss of generality we use self-signed certificates to make things simple
in tests.
"""
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.backends.interfaces import (
DSABackend, EllipticCurveBackend, RSABackend, X509Backend
)
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa
from cryptography.x509.oid import NameOID
from datetime import datetime, timedelta

from helpers import tf_cfg

__author__ = 'Tempesta Technologies, Inc.'
__copyright__ = 'Copyright (C) 2019 Tempesta Technologies, Inc.'
__license__ = 'GPL2'


class CertGenerator:

def __init__(self, cert_path=None, key_path=None, default=False):
workdir = tf_cfg.cfg.get('General', 'workdir')
self.f_cert = cert_path if cert_path else workdir + "/tempesta.crt"
self.f_key = key_path if key_path else workdir + "/tempesta.key"
# Define the certificate fields data supposed for mutation by a caller.
self.C = u'US'
self.ST = u'Washington'
self.L = u'Seattle'
self.O = u'Tempesta Technologies Inc.'
self.OU = u'Testing'
self.CN = u'tempesta-tech.com'
self.emailAddress = u'info@tempesta-tech.com'
self.not_valid_before = datetime.now() - timedelta(1)
self.not_valid_after = datetime.now() + timedelta(365)
# Use EC by defauls as the fastest and more widespread.
self.key = {
'alg': 'ecdsa',
'curve': ec.SECP256R1()
}
self.sign_alg = 'sha256'
self.format = 'pem'
self.cert = None
self.pkey = None
if default:
self.generate()

@staticmethod
def __write(path, data):
fdesc = open(path, "wt")
fdesc.write(data)
fdesc.close()

def __encoding(self):
if self.format == 'pem':
return serialization.Encoding.PEM
else:
raise NotImplementedError("Not implemented encoding: %s"
% self.format)

def __build_name(self):
return x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, self.C),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, self.ST),
x509.NameAttribute(NameOID.LOCALITY_NAME, self.L),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, self.O),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, self.OU),
x509.NameAttribute(NameOID.COMMON_NAME, self.CN),
x509.NameAttribute(NameOID.EMAIL_ADDRESS, self.emailAddress),
])

def __gen_key_pair(self):
if self.key['alg'] == 'rsa':
assert self.key['len'], "No RSA key length specified"
self.pkey = rsa.generate_private_key(65537, self.key['len'],
default_backend())
elif self.key['alg'] == 'ecdsa':
assert self.key['curve'], "No EC curve specified"
self.pkey = ec.generate_private_key(self.key['curve'],
default_backend())
else:
raise NotImplementedError("Not implemented key algorithm: %s"
% self.key_alg)

def __hash(self):
if self.sign_alg == 'sha1':
return hashes.SHA1()
elif self.sign_alg == 'sha256':
return hashes.SHA256()
elif self.sign_alg == 'sha384':
return hashes.SHA384()
elif self.sign_alg == 'sha512':
return hashes.SHA512()
else:
raise NotImplementedError("Not implemented hash algorithm: %s"
% self.sign_alg)

def serialize_cert(self):
return self.cert.public_bytes(self.__encoding())

def serialize_priv_key(self):
return self.pkey.private_bytes(self.__encoding(),
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption())

def generate(self):
self.__gen_key_pair()
x509name = self.__build_name()
builder = x509.CertificateBuilder().serial_number(
x509.random_serial_number()
).subject_name(
x509name
).issuer_name(
x509name
).not_valid_before(
self.not_valid_before
).not_valid_after(
self.not_valid_after
).public_key(
self.pkey.public_key()
)
self.cert = builder.sign(self.pkey, self.__hash(), default_backend())
# Write the certificate & private key.
self.__write(self.f_cert, self.serialize_cert())
self.__write(self.f_key, self.serialize_priv_key())

def __str__(self):
assert self.cert, "Stringify null x509 certificate object"
return str(self.cert)

def get_file_paths(self):
return (self.f_cert, self.f_key)
Loading