]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
tests: improve test_17_07_ssl_ciphers
authorJan Venekamp <1422460+jan2000@users.noreply.github.com>
Tue, 20 Aug 2024 00:53:19 +0000 (02:53 +0200)
committerDaniel Stenberg <daniel@haxx.se>
Tue, 20 Aug 2024 10:46:13 +0000 (12:46 +0200)
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

tests/http/test_17_ssl_use.py
tests/http/test_18_methods.py
tests/http/testenv/httpd.py

index 6f76852eb98e60475f1135538240178fc4cddfa2..2bf2aa51e21940b548cebf3e21ccfa9ea3be638b 100644 (file)
@@ -41,19 +41,17 @@ log = logging.getLogger(__name__)
 class TestSSLUse:
 
     @pytest.fixture(autouse=True, scope='class')
-    def _class_scope(self, env, httpd, nghttpx):
+    def _class_scope(self, env, nghttpx):
         if env.have_h3():
             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'
         curl = CurlClient(env=env)
         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}'
 
     @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'
         count = 3
         exp_resumed = 'Resumed'
@@ -108,7 +106,7 @@ class TestSSLUse:
 
     # use host name with trailing dot, verify handshake
     @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():
             pytest.skip("h3 not supported")
         curl = CurlClient(env=env)
@@ -123,7 +121,7 @@ class TestSSLUse:
 
     # use host name with double trailing dot, verify handshake
     @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():
             pytest.skip("h3 not supported")
         if proto == 'h3' and env.curl_uses_lib('wolfssl'):
@@ -147,7 +145,7 @@ class TestSSLUse:
 
     # use ip address for connect
     @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'):
             pytest.skip("BearSSL does not support cert verification with IP addresses")
         if env.curl_uses_lib('mbedtls'):
@@ -166,7 +164,7 @@ class TestSSLUse:
 
     # use localhost for connect
     @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():
             pytest.skip("h3 not supported")
         curl = CurlClient(env=env)
@@ -178,66 +176,82 @@ class TestSSLUse:
         if proto != 'h3':  # we proxy h3
             assert r.json['SSL_TLS_SNI'] == domain, f'{r.json}'
 
-    # test setting cipher suites, the AES 256 ciphers are disabled in the test server
-    @pytest.mark.parametrize("ciphers, succeed", [
-        [[0x1301], True],
-        [[0x1302], False],
-        [[0x1303], True],
-        [[0x1302, 0x1303], True],
-        [[0xC02B, 0xC02F], True],
-        [[0xC02C, 0xC030], False],
-        [[0xCCA9, 0xCCA8], True],
-        [[0xC02C, 0xC030, 0xCCA9, 0xCCA8], True],
-    ])
-    def test_17_07_ssl_ciphers(self, env: Env, httpd, nghttpx, ciphers, succeed, repeat):
-        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))
+    @staticmethod
+    def gen_test_17_07_list():
+        tls13_tests = [
+            [None, True],
+            [['TLS_AES_128_GCM_SHA256'], True],
+            [['TLS_AES_256_GCM_SHA384'], False],
+            [['TLS_CHACHA20_POLY1305_SHA256'], True],
+            [['TLS_AES_256_GCM_SHA384',
+              'TLS_CHACHA20_POLY1305_SHA256'], 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}'
+        ])
+        httpd.reload_if_config_changed()
         proto = 'http/1.1'
         curl = CurlClient(env=env)
         url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
-        extra_args = []
+        # SSL backend specifics
         if env.curl_uses_lib('gnutls'):
-            pytest.skip('GnuTLS does not support setting ciphers by name')
-        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'):
+            pytest.skip('GnuTLS does not support setting ciphers')
+        elif env.curl_uses_lib('boringssl'):
+            if ciphers13 is not None:
                 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'):
-                pytest.skip('mbedTLS TLSv1.3 support requires at least 3.6.0')
-            else:
-                extra_args = ['--tls13-ciphers', ':'.join(cipher_names)]
-        else:
-            # test setting TLSv1.2 ciphers
-            if env.curl_uses_lib('schannel'):
+        elif env.curl_uses_lib('schannel'):  # not in CI, so untested
+            if ciphers12 is not None:
                 pytest.skip('Schannel does not support setting TLSv1.2 ciphers by name')
-            else:
-                # the server supports TLSv1.3, so to test TLSv1.2 ciphers we set tls-max
-                extra_args = ['--tls-max', '1.2', '--ciphers', ':'.join(cipher_names)]
+        elif env.curl_uses_lib('bearssl'):
+            if tls_proto == 'TLSv1.3':
+                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)
-        if succeed:
-            assert r.exit_code == 0, f'{r}'
-            assert r.json['HTTPS'] == 'on', f'{r.json}'
-            assert 'SSL_CIPHER' in r.json, f'{r.json}'
-            assert r.json['SSL_CIPHER'] in cipher_names, f'{r.json}'
+        if tls_proto != 'TLSv1.2' and succeed13:
+            assert r.exit_code == 0, r.dump_logs()
+            assert r.json['HTTPS'] == 'on', r.dump_logs()
+            assert r.json['SSL_PROTOCOL'] == 'TLSv1.3', r.dump_logs()
+            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:
-            assert r.exit_code != 0, f'{r}'
+            assert r.exit_code != 0, r.dump_logs()
 
     @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():
             pytest.skip("h3 not supported")
         if not env.curl_uses_lib('openssl') and \
index e959f19aa5e2edc8d3c116d502187d3fdf8ed575..ed9f47729d98287ffd79dea6bf71cea707e13d1e 100644 (file)
@@ -44,10 +44,7 @@ class TestMethods:
         if env.have_h3():
             nghttpx.start_if_needed()
         httpd.clear_extra_configs()
-        httpd.reload()
-
-    @pytest.fixture(autouse=True, scope='class')
-    def _class_scope(self, env, httpd):
+        httpd.reload_if_config_changed()
         indir = httpd.docs_dir
         env.make_data_file(indir=indir, fname="data-10k", fsize=10*1024)
         env.make_data_file(indir=indir, fname="data-100k", fsize=100*1024)
index 7b6cd6618dae2dcee61a879d0fe8414f39ae2a52..8cc2c34a5f1f08358b72ef1fc71233e03ec40bdd 100644 (file)
@@ -1,5 +1,4 @@
 #!/usr/bin/env python3
-#!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 #***************************************************************************
 #                                  _   _ ____  _
@@ -33,6 +32,7 @@ from datetime import timedelta, datetime
 from json import JSONEncoder
 import time
 from typing import List, Union, Optional
+import copy
 
 from .curl import CurlClient, ExecResult
 from .env import Env
@@ -79,6 +79,7 @@ class Httpd:
         self._auth_digest = True
         self._proxy_auth_basic = proxy_auth
         self._extra_configs = {}
+        self._loaded_extra_configs = None
         assert env.apxs
         p = subprocess.run(args=[env.apxs, '-q', 'libexecdir'],
                            capture_output=True, text=True)
@@ -152,10 +153,12 @@ class Httpd:
         if r.exit_code != 0:
             log.error(f'failed to start httpd: {r}')
             return False
+        self._loaded_extra_configs = copy.deepcopy(self._extra_configs)
         return self.wait_live(timeout=timedelta(seconds=5))
 
     def stop(self):
         r = self._apachectl('stop')
+        self._loaded_extra_configs = None
         if r.exit_code == 0:
             return self.wait_dead(timeout=timedelta(seconds=5))
         log.fatal(f'stopping httpd failed: {r}')
@@ -168,10 +171,17 @@ class Httpd:
     def reload(self):
         self._write_config()
         r = self._apachectl("graceful")
+        self._loaded_extra_configs = None
         if r.exit_code != 0:
             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))
 
+    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):
         curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
         try_until = datetime.now() + timeout