tests: improve test_17_07_ssl_ciphers

Change TLS proto version on the test httpd server to test setting
combinations of --tls13-ciphers and --ciphers.

To not let the changed config of the httpd server bleed into the next
test, clean and reload on each test. Because a reload is slow, only
do this if the config is different than the loaded config. For this
the httpd.reload_if_config_changed() method is added.

Overloading of autouse fixtures does not seem to work. For the test
httpd server to be reloaded with a clean config in test_18_methods,
to not be affected by the config changes in test_17_ssl_use, the two
class scope fixtures of test_18_methods are now combined.

Closes #14589
This commit is contained in:
Jan Venekamp 2024-08-20 02:53:19 +02:00 committed by Daniel Stenberg
parent 925aea1aba
commit 3ca38f9a5e
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2
3 changed files with 91 additions and 70 deletions

View File

@ -41,19 +41,17 @@ log = logging.getLogger(__name__)
class TestSSLUse: class TestSSLUse:
@pytest.fixture(autouse=True, scope='class') @pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env, httpd, nghttpx): def _class_scope(self, env, nghttpx):
if env.have_h3(): if env.have_h3():
nghttpx.start_if_needed() nghttpx.start_if_needed()
httpd.set_extra_config('base', [
f'SSLCipherSuite SSL'\
f' ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'\
f':ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305',
f'SSLCipherSuite TLSv1.3'\
f' TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256',
])
httpd.reload()
def test_17_01_sslinfo_plain(self, env: Env, httpd, nghttpx, repeat): @pytest.fixture(autouse=True, scope='function')
def _function_scope(self, request, env, httpd):
httpd.clear_extra_configs()
if 'httpd' not in request.node._fixtureinfo.argnames:
httpd.reload_if_config_changed()
def test_17_01_sslinfo_plain(self, env: Env, nghttpx, repeat):
proto = 'http/1.1' proto = 'http/1.1'
curl = CurlClient(env=env) curl = CurlClient(env=env)
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo' url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
@ -64,7 +62,7 @@ class TestSSLUse:
assert r.json['SSL_SESSION_RESUMED'] == 'Initial', f'{r.json}' assert r.json['SSL_SESSION_RESUMED'] == 'Initial', f'{r.json}'
@pytest.mark.parametrize("tls_max", ['1.2', '1.3']) @pytest.mark.parametrize("tls_max", ['1.2', '1.3'])
def test_17_02_sslinfo_reconnect(self, env: Env, httpd, nghttpx, tls_max, repeat): def test_17_02_sslinfo_reconnect(self, env: Env, tls_max):
proto = 'http/1.1' proto = 'http/1.1'
count = 3 count = 3
exp_resumed = 'Resumed' exp_resumed = 'Resumed'
@ -108,7 +106,7 @@ class TestSSLUse:
# use host name with trailing dot, verify handshake # use host name with trailing dot, verify handshake
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
def test_17_03_trailing_dot(self, env: Env, httpd, nghttpx, repeat, proto): def test_17_03_trailing_dot(self, env: Env, proto):
if proto == 'h3' and not env.have_h3(): if proto == 'h3' and not env.have_h3():
pytest.skip("h3 not supported") pytest.skip("h3 not supported")
curl = CurlClient(env=env) curl = CurlClient(env=env)
@ -123,7 +121,7 @@ class TestSSLUse:
# use host name with double trailing dot, verify handshake # use host name with double trailing dot, verify handshake
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
def test_17_04_double_dot(self, env: Env, httpd, nghttpx, repeat, proto): def test_17_04_double_dot(self, env: Env, proto):
if proto == 'h3' and not env.have_h3(): if proto == 'h3' and not env.have_h3():
pytest.skip("h3 not supported") pytest.skip("h3 not supported")
if proto == 'h3' and env.curl_uses_lib('wolfssl'): if proto == 'h3' and env.curl_uses_lib('wolfssl'):
@ -147,7 +145,7 @@ class TestSSLUse:
# use ip address for connect # use ip address for connect
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
def test_17_05_ip_addr(self, env: Env, httpd, nghttpx, repeat, proto): def test_17_05_ip_addr(self, env: Env, proto):
if env.curl_uses_lib('bearssl'): if env.curl_uses_lib('bearssl'):
pytest.skip("BearSSL does not support cert verification with IP addresses") pytest.skip("BearSSL does not support cert verification with IP addresses")
if env.curl_uses_lib('mbedtls'): if env.curl_uses_lib('mbedtls'):
@ -166,7 +164,7 @@ class TestSSLUse:
# use localhost for connect # use localhost for connect
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
def test_17_06_localhost(self, env: Env, httpd, nghttpx, repeat, proto): def test_17_06_localhost(self, env: Env, proto):
if proto == 'h3' and not env.have_h3(): if proto == 'h3' and not env.have_h3():
pytest.skip("h3 not supported") pytest.skip("h3 not supported")
curl = CurlClient(env=env) curl = CurlClient(env=env)
@ -178,66 +176,82 @@ class TestSSLUse:
if proto != 'h3': # we proxy h3 if proto != 'h3': # we proxy h3
assert r.json['SSL_TLS_SNI'] == domain, f'{r.json}' assert r.json['SSL_TLS_SNI'] == domain, f'{r.json}'
# test setting cipher suites, the AES 256 ciphers are disabled in the test server @staticmethod
@pytest.mark.parametrize("ciphers, succeed", [ def gen_test_17_07_list():
[[0x1301], True], tls13_tests = [
[[0x1302], False], [None, True],
[[0x1303], True], [['TLS_AES_128_GCM_SHA256'], True],
[[0x1302, 0x1303], True], [['TLS_AES_256_GCM_SHA384'], False],
[[0xC02B, 0xC02F], True], [['TLS_CHACHA20_POLY1305_SHA256'], True],
[[0xC02C, 0xC030], False], [['TLS_AES_256_GCM_SHA384',
[[0xCCA9, 0xCCA8], True], 'TLS_CHACHA20_POLY1305_SHA256'], True],
[[0xC02C, 0xC030, 0xCCA9, 0xCCA8], True], ]
tls12_tests = [
[None, True],
[['ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-GCM-SHA256'], True],
[['ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES256-GCM-SHA384'], False],
[['ECDHE-ECDSA-CHACHA20-POLY1305', 'ECDHE-RSA-CHACHA20-POLY1305'], True],
[['ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-CHACHA20-POLY1305', 'ECDHE-RSA-CHACHA20-POLY1305'], True],
]
ret = []
for tls_proto in ['TLSv1.3 +TLSv1.2', 'TLSv1.3', 'TLSv1.2']:
for [ciphers13, succeed13] in tls13_tests:
for [ciphers12, succeed12] in tls12_tests:
ret.append([tls_proto, ciphers13, ciphers12, succeed13, succeed12])
return ret
@pytest.mark.parametrize("tls_proto, ciphers13, ciphers12, succeed13, succeed12", gen_test_17_07_list())
def test_17_07_ssl_ciphers(self, env: Env, httpd, tls_proto, ciphers13, ciphers12, succeed13, succeed12):
# to test setting cipher suites, the AES 256 ciphers are disabled in the test server
httpd.set_extra_config('base', [
'SSLCipherSuite SSL'
' ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'
':ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305',
'SSLCipherSuite TLSv1.3'
' TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256',
f'SSLProtocol {tls_proto}'
]) ])
def test_17_07_ssl_ciphers(self, env: Env, httpd, nghttpx, ciphers, succeed, repeat): httpd.reload_if_config_changed()
cipher_table = {
0x1301: 'TLS_AES_128_GCM_SHA256',
0x1302: 'TLS_AES_256_GCM_SHA384',
0x1303: 'TLS_CHACHA20_POLY1305_SHA256',
0xC02B: 'ECDHE-ECDSA-AES128-GCM-SHA256',
0xC02F: 'ECDHE-RSA-AES128-GCM-SHA256',
0xC02C: 'ECDHE-ECDSA-AES256-GCM-SHA384',
0xC030: 'ECDHE-RSA-AES256-GCM-SHA384',
0xCCA9: 'ECDHE-ECDSA-CHACHA20-POLY1305',
0xCCA8: 'ECDHE-RSA-CHACHA20-POLY1305',
}
cipher_names = list(map(cipher_table.get, ciphers))
proto = 'http/1.1' proto = 'http/1.1'
curl = CurlClient(env=env) curl = CurlClient(env=env)
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo' url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
extra_args = [] # SSL backend specifics
if env.curl_uses_lib('gnutls'): if env.curl_uses_lib('gnutls'):
pytest.skip('GnuTLS does not support setting ciphers by name') pytest.skip('GnuTLS does not support setting ciphers')
if ciphers[0] & 0xFF00 == 0x1300:
# test setting TLSv1.3 ciphers
if env.curl_uses_lib('bearssl'):
pytest.skip('BearSSL does not support TLSv1.3')
elif env.curl_uses_lib('sectransp'):
pytest.skip('SecureTransport does not support TLSv1.3')
elif env.curl_uses_lib('boringssl'): elif env.curl_uses_lib('boringssl'):
if ciphers13 is not None:
pytest.skip('BoringSSL does not support setting TLSv1.3 ciphers') pytest.skip('BoringSSL does not support setting TLSv1.3 ciphers')
elif env.curl_uses_lib('mbedtls') and not env.curl_lib_version_at_least('mbedtls', '3.6.0'): elif env.curl_uses_lib('schannel'): # not in CI, so untested
pytest.skip('mbedTLS TLSv1.3 support requires at least 3.6.0') if ciphers12 is not None:
else:
extra_args = ['--tls13-ciphers', ':'.join(cipher_names)]
else:
# test setting TLSv1.2 ciphers
if env.curl_uses_lib('schannel'):
pytest.skip('Schannel does not support setting TLSv1.2 ciphers by name') pytest.skip('Schannel does not support setting TLSv1.2 ciphers by name')
else: elif env.curl_uses_lib('bearssl'):
# the server supports TLSv1.3, so to test TLSv1.2 ciphers we set tls-max if tls_proto == 'TLSv1.3':
extra_args = ['--tls-max', '1.2', '--ciphers', ':'.join(cipher_names)] pytest.skip('BearSSL does not support TLSv1.3')
tls_proto = 'TLSv1.2'
elif env.curl_uses_lib('sectransp'): # not in CI, so untested
if tls_proto == 'TLSv1.3':
pytest.skip('SecureTransport does not support TLSv1.3')
tls_proto = 'TLSv1.2'
# test
extra_args = ['--tls13-ciphers', ':'.join(ciphers13)] if ciphers13 else []
extra_args += ['--ciphers', ':'.join(ciphers12)] if ciphers12 else []
r = curl.http_get(url=url, alpn_proto=proto, extra_args=extra_args) r = curl.http_get(url=url, alpn_proto=proto, extra_args=extra_args)
if succeed: if tls_proto != 'TLSv1.2' and succeed13:
assert r.exit_code == 0, f'{r}' assert r.exit_code == 0, r.dump_logs()
assert r.json['HTTPS'] == 'on', f'{r.json}' assert r.json['HTTPS'] == 'on', r.dump_logs()
assert 'SSL_CIPHER' in r.json, f'{r.json}' assert r.json['SSL_PROTOCOL'] == 'TLSv1.3', r.dump_logs()
assert r.json['SSL_CIPHER'] in cipher_names, f'{r.json}' assert ciphers13 is None or r.json['SSL_CIPHER'] in ciphers13, r.dump_logs()
elif tls_proto == 'TLSv1.2' and succeed12:
assert r.exit_code == 0, r.dump_logs()
assert r.json['HTTPS'] == 'on', r.dump_logs()
assert r.json['SSL_PROTOCOL'] == 'TLSv1.2', r.dump_logs()
assert ciphers12 is None or r.json['SSL_CIPHER'] in ciphers12, r.dump_logs()
else: else:
assert r.exit_code != 0, f'{r}' assert r.exit_code != 0, r.dump_logs()
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
def test_17_08_cert_status(self, env: Env, httpd, nghttpx, repeat, proto): def test_17_08_cert_status(self, env: Env, proto):
if proto == 'h3' and not env.have_h3(): if proto == 'h3' and not env.have_h3():
pytest.skip("h3 not supported") pytest.skip("h3 not supported")
if not env.curl_uses_lib('openssl') and \ if not env.curl_uses_lib('openssl') and \

View File

@ -44,10 +44,7 @@ class TestMethods:
if env.have_h3(): if env.have_h3():
nghttpx.start_if_needed() nghttpx.start_if_needed()
httpd.clear_extra_configs() httpd.clear_extra_configs()
httpd.reload() httpd.reload_if_config_changed()
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env, httpd):
indir = httpd.docs_dir indir = httpd.docs_dir
env.make_data_file(indir=indir, fname="data-10k", fsize=10*1024) env.make_data_file(indir=indir, fname="data-10k", fsize=10*1024)
env.make_data_file(indir=indir, fname="data-100k", fsize=100*1024) env.make_data_file(indir=indir, fname="data-100k", fsize=100*1024)

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
#!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
#*************************************************************************** #***************************************************************************
# _ _ ____ _ # _ _ ____ _
@ -33,6 +32,7 @@ from datetime import timedelta, datetime
from json import JSONEncoder from json import JSONEncoder
import time import time
from typing import List, Union, Optional from typing import List, Union, Optional
import copy
from .curl import CurlClient, ExecResult from .curl import CurlClient, ExecResult
from .env import Env from .env import Env
@ -79,6 +79,7 @@ class Httpd:
self._auth_digest = True self._auth_digest = True
self._proxy_auth_basic = proxy_auth self._proxy_auth_basic = proxy_auth
self._extra_configs = {} self._extra_configs = {}
self._loaded_extra_configs = None
assert env.apxs assert env.apxs
p = subprocess.run(args=[env.apxs, '-q', 'libexecdir'], p = subprocess.run(args=[env.apxs, '-q', 'libexecdir'],
capture_output=True, text=True) capture_output=True, text=True)
@ -152,10 +153,12 @@ class Httpd:
if r.exit_code != 0: if r.exit_code != 0:
log.error(f'failed to start httpd: {r}') log.error(f'failed to start httpd: {r}')
return False return False
self._loaded_extra_configs = copy.deepcopy(self._extra_configs)
return self.wait_live(timeout=timedelta(seconds=5)) return self.wait_live(timeout=timedelta(seconds=5))
def stop(self): def stop(self):
r = self._apachectl('stop') r = self._apachectl('stop')
self._loaded_extra_configs = None
if r.exit_code == 0: if r.exit_code == 0:
return self.wait_dead(timeout=timedelta(seconds=5)) return self.wait_dead(timeout=timedelta(seconds=5))
log.fatal(f'stopping httpd failed: {r}') log.fatal(f'stopping httpd failed: {r}')
@ -168,10 +171,17 @@ class Httpd:
def reload(self): def reload(self):
self._write_config() self._write_config()
r = self._apachectl("graceful") r = self._apachectl("graceful")
self._loaded_extra_configs = None
if r.exit_code != 0: if r.exit_code != 0:
log.error(f'failed to reload httpd: {r}') log.error(f'failed to reload httpd: {r}')
self._loaded_extra_configs = copy.deepcopy(self._extra_configs)
return self.wait_live(timeout=timedelta(seconds=5)) return self.wait_live(timeout=timedelta(seconds=5))
def reload_if_config_changed(self):
if self._loaded_extra_configs == self._extra_configs:
return True
return self.reload()
def wait_dead(self, timeout: timedelta): def wait_dead(self, timeout: timedelta):
curl = CurlClient(env=self.env, run_dir=self._tmp_dir) curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
try_until = datetime.now() + timeout try_until = datetime.now() + timeout