summaryrefslogtreecommitdiffstats
path: root/test/modules/tls
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-25 04:41:27 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-25 04:41:27 +0000
commitc54018b07a9085c0a3aedbc2bd01a85a3b3e20cf (patch)
treef6e1d6fcf9f6db3794c418b2f89ecf9e08ff41c8 /test/modules/tls
parentAdding debian version 2.4.38-3+deb10u10. (diff)
downloadapache2-c54018b07a9085c0a3aedbc2bd01a85a3b3e20cf.tar.xz
apache2-c54018b07a9085c0a3aedbc2bd01a85a3b3e20cf.zip
Merging upstream version 2.4.59.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'test/modules/tls')
-rw-r--r--test/modules/tls/__init__.py0
-rw-r--r--test/modules/tls/conf.py61
-rw-r--r--test/modules/tls/conftest.py39
-rw-r--r--test/modules/tls/env.py190
-rw-r--r--test/modules/tls/htdocs/a.mod-tls.test/index.json3
-rwxr-xr-xtest/modules/tls/htdocs/a.mod-tls.test/vars.py56
-rwxr-xr-xtest/modules/tls/htdocs/b.mod-tls.test/dir1/vars.py23
-rw-r--r--test/modules/tls/htdocs/b.mod-tls.test/index.json3
-rwxr-xr-xtest/modules/tls/htdocs/b.mod-tls.test/resp-jitter.py23
-rwxr-xr-xtest/modules/tls/htdocs/b.mod-tls.test/vars.py56
-rw-r--r--test/modules/tls/htdocs/index.html9
-rw-r--r--test/modules/tls/htdocs/index.json3
-rw-r--r--test/modules/tls/test_01_apache.py14
-rw-r--r--test/modules/tls/test_02_conf.py138
-rw-r--r--test/modules/tls/test_03_sni.py71
-rw-r--r--test/modules/tls/test_04_get.py67
-rw-r--r--test/modules/tls/test_05_proto.py64
-rw-r--r--test/modules/tls/test_06_ciphers.py209
-rw-r--r--test/modules/tls/test_07_alpn.py43
-rw-r--r--test/modules/tls/test_08_vars.py60
-rw-r--r--test/modules/tls/test_09_timeout.py43
-rw-r--r--test/modules/tls/test_10_session_id.py50
-rw-r--r--test/modules/tls/test_11_md.py37
-rw-r--r--test/modules/tls/test_12_cauth.py235
-rw-r--r--test/modules/tls/test_13_proxy.py40
-rw-r--r--test/modules/tls/test_14_proxy_ssl.py78
-rw-r--r--test/modules/tls/test_15_proxy_tls.py86
-rw-r--r--test/modules/tls/test_16_proxy_mixed.py47
-rw-r--r--test/modules/tls/test_17_proxy_machine_cert.py69
29 files changed, 1817 insertions, 0 deletions
diff --git a/test/modules/tls/__init__.py b/test/modules/tls/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/modules/tls/__init__.py
diff --git a/test/modules/tls/conf.py b/test/modules/tls/conf.py
new file mode 100644
index 0000000..ddeb91f
--- /dev/null
+++ b/test/modules/tls/conf.py
@@ -0,0 +1,61 @@
+import os
+from typing import List, Dict, Any
+
+from pyhttpd.conf import HttpdConf
+from pyhttpd.env import HttpdTestEnv
+
+
+class TlsTestConf(HttpdConf):
+
+ def __init__(self, env: HttpdTestEnv, extras: Dict[str, Any] = None):
+ extras = extras if extras is not None else {}
+ super().__init__(env=env, extras=extras)
+
+ def start_tls_vhost(self, domains: List[str], port=None, ssl_module=None):
+ if ssl_module is None:
+ ssl_module = 'mod_tls'
+ super().start_vhost(domains=domains, port=port, doc_root=f"htdocs/{domains[0]}", ssl_module=ssl_module)
+
+ def end_tls_vhost(self):
+ self.end_vhost()
+
+ def add_tls_vhosts(self, domains: List[str], port=None, ssl_module=None):
+ for domain in domains:
+ self.start_tls_vhost(domains=[domain], port=port, ssl_module=ssl_module)
+ self.end_tls_vhost()
+
+ def add_md_vhosts(self, domains: List[str], port = None):
+ self.add([
+ f"LoadModule md_module {self.env.libexec_dir}/mod_md.so",
+ "LogLevel md:debug",
+ ])
+ for domain in domains:
+ self.add(f"<MDomain {domain}>")
+ for cred in self.env.ca.get_credentials_for_name(domain):
+ cert_file = os.path.relpath(cred.cert_file, self.env.server_dir)
+ pkey_file = os.path.relpath(cred.pkey_file, self.env.server_dir) if cred.pkey_file else cert_file
+ self.add([
+ f" MDCertificateFile {cert_file}",
+ f" MDCertificateKeyFile {pkey_file}",
+ ])
+ self.add("</MDomain>")
+ super().add_vhost(domains=[domain], port=port, doc_root=f"htdocs/{domain}",
+ with_ssl=True, with_certificates=False, ssl_module='mod_tls')
+
+ def add_md_base(self, domain: str):
+ self.add([
+ f"LoadModule md_module {self.env.libexec_dir}/mod_md.so",
+ "LogLevel md:debug",
+ f"ServerName {domain}",
+ "MDBaseServer on",
+ ])
+ self.add(f"TLSEngine {self.env.https_port}")
+ self.add(f"<MDomain {domain}>")
+ for cred in self.env.ca.get_credentials_for_name(domain):
+ cert_file = os.path.relpath(cred.cert_file, self.env.server_dir)
+ pkey_file = os.path.relpath(cred.pkey_file, self.env.server_dir) if cred.pkey_file else cert_file
+ self.add([
+ f"MDCertificateFile {cert_file}",
+ f"MDCertificateKeyFile {pkey_file}",
+ ])
+ self.add("</MDomain>")
diff --git a/test/modules/tls/conftest.py b/test/modules/tls/conftest.py
new file mode 100644
index 0000000..cde4be6
--- /dev/null
+++ b/test/modules/tls/conftest.py
@@ -0,0 +1,39 @@
+import logging
+import os
+import sys
+import pytest
+
+sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+from .env import TlsTestEnv
+
+
+def pytest_report_header(config, startdir):
+ _x = config
+ _x = startdir
+ env = TlsTestEnv()
+ return "mod_tls [apache: {aversion}({prefix})]".format(
+ prefix=env.prefix,
+ aversion=env.get_httpd_version()
+ )
+
+
+@pytest.fixture(scope="package")
+def env(pytestconfig) -> TlsTestEnv:
+ level = logging.INFO
+ console = logging.StreamHandler()
+ console.setLevel(level)
+ console.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
+ logging.getLogger('').addHandler(console)
+ logging.getLogger('').setLevel(level=level)
+ env = TlsTestEnv(pytestconfig=pytestconfig)
+ env.setup_httpd()
+ env.apache_access_log_clear()
+ env.httpd_error_log.clear_log()
+ return env
+
+
+@pytest.fixture(autouse=True, scope="package")
+def _session_scope(env):
+ yield
+ assert env.apache_stop() == 0
diff --git a/test/modules/tls/env.py b/test/modules/tls/env.py
new file mode 100644
index 0000000..0e457bf
--- /dev/null
+++ b/test/modules/tls/env.py
@@ -0,0 +1,190 @@
+import inspect
+import logging
+import os
+import re
+import subprocess
+
+from datetime import timedelta, datetime
+from typing import List, Optional, Dict, Tuple, Union
+
+from pyhttpd.certs import CertificateSpec
+from pyhttpd.env import HttpdTestEnv, HttpdTestSetup
+from pyhttpd.result import ExecResult
+
+log = logging.getLogger(__name__)
+
+
+class TlsTestSetup(HttpdTestSetup):
+
+ def __init__(self, env: 'HttpdTestEnv'):
+ super().__init__(env=env)
+ self.add_source_dir(os.path.dirname(inspect.getfile(TlsTestSetup)))
+ self.add_modules(["tls", "http2", "cgid", "watchdog", "proxy_http2"])
+
+
+class TlsCipher:
+
+ def __init__(self, id: int, name: str, flavour: str,
+ min_version: float, max_version: float = None,
+ openssl: str = None):
+ self.id = id
+ self.name = name
+ self.flavour = flavour
+ self.min_version = min_version
+ self.max_version = max_version if max_version is not None else self.min_version
+ if openssl is None:
+ if name.startswith('TLS13_'):
+ openssl = re.sub(r'^TLS13_', 'TLS_', name)
+ else:
+ openssl = re.sub(r'^TLS_', '', name)
+ openssl = re.sub(r'_WITH_([^_]+)_', r'_\1_', openssl)
+ openssl = re.sub(r'_AES_(\d+)', r'_AES\1', openssl)
+ openssl = re.sub(r'(_POLY1305)_\S+$', r'\1', openssl)
+ openssl = re.sub(r'_', '-', openssl)
+ self.openssl_name = openssl
+ self.id_name = "TLS_CIPHER_0x{0:04x}".format(self.id)
+
+ def __repr__(self):
+ return self.name
+
+ def __str__(self):
+ return self.name
+
+
+class TlsTestEnv(HttpdTestEnv):
+
+ CURL_SUPPORTS_TLS_1_3 = None
+
+ @classmethod
+ def curl_supports_tls_1_3(cls) -> bool:
+ if cls.CURL_SUPPORTS_TLS_1_3 is None:
+ # Unfortunately, there is no reliable, platform-independant
+ # way to verify that TLSv1.3 is properly supported by curl.
+ #
+ # p = subprocess.run(['curl', '--tlsv1.3', 'https://shouldneverexistreally'],
+ # stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+ # return code 6 means the site could not be resolved, but the
+ # tls parameter was recognized
+ cls.CURL_SUPPORTS_TLS_1_3 = False
+ return cls.CURL_SUPPORTS_TLS_1_3
+
+
+ # current rustls supported ciphers in their order of preference
+ # used to test cipher selection, see test_06_ciphers.py
+ RUSTLS_CIPHERS = [
+ TlsCipher(0x1303, "TLS13_CHACHA20_POLY1305_SHA256", "CHACHA", 1.3),
+ TlsCipher(0x1302, "TLS13_AES_256_GCM_SHA384", "AES", 1.3),
+ TlsCipher(0x1301, "TLS13_AES_128_GCM_SHA256", "AES", 1.3),
+ TlsCipher(0xcca9, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", "ECDSA", 1.2),
+ TlsCipher(0xcca8, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", "RSA", 1.2),
+ TlsCipher(0xc02c, "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "ECDSA", 1.2),
+ TlsCipher(0xc02b, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "ECDSA", 1.2),
+ TlsCipher(0xc030, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "RSA", 1.2),
+ TlsCipher(0xc02f, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "RSA", 1.2),
+ ]
+
+ def __init__(self, pytestconfig=None):
+ super().__init__(pytestconfig=pytestconfig)
+ self._domain_a = "a.mod-tls.test"
+ self._domain_b = "b.mod-tls.test"
+ self.add_httpd_conf([
+ f'<Directory "{self.server_dir}/htdocs/{self.domain_a}">',
+ ' AllowOverride None',
+ ' Require all granted',
+ ' AddHandler cgi-script .py',
+ ' Options +ExecCGI',
+ '</Directory>',
+ f'<Directory "{self.server_dir}/htdocs/{self.domain_b}">',
+ ' AllowOverride None',
+ ' Require all granted',
+ ' AddHandler cgi-script .py',
+ ' Options +ExecCGI',
+ '</Directory>',
+ f'<VirtualHost *:{self.http_port}>',
+ ' ServerName localhost',
+ ' DocumentRoot "htdocs"',
+ '</VirtualHost>',
+ f'<VirtualHost *:{self.http_port}>',
+ f' ServerName {self.domain_a}',
+ ' DocumentRoot "htdocs/a.mod-tls.test"',
+ '</VirtualHost>',
+ f'<VirtualHost *:{self.http_port}>',
+ f' ServerName {self.domain_b}',
+ ' DocumentRoot "htdocs/b.mod-tls.test"',
+ '</VirtualHost>',
+ ])
+ self.add_cert_specs([
+ CertificateSpec(domains=[self.domain_a]),
+ CertificateSpec(domains=[self.domain_b], key_type='secp256r1', single_file=True),
+ CertificateSpec(domains=[self.domain_b], key_type='rsa4096'),
+ CertificateSpec(name="clientsX", sub_specs=[
+ CertificateSpec(name="user1", client=True, single_file=True),
+ CertificateSpec(name="user2", client=True, single_file=True),
+ CertificateSpec(name="user_expired", client=True,
+ single_file=True, valid_from=timedelta(days=-91),
+ valid_to=timedelta(days=-1)),
+ ]),
+ CertificateSpec(name="clientsY", sub_specs=[
+ CertificateSpec(name="user1", client=True, single_file=True),
+ ]),
+ CertificateSpec(name="user1", client=True, single_file=True),
+ ])
+ self.add_httpd_log_modules(['tls'])
+
+
+ def setup_httpd(self, setup: TlsTestSetup = None):
+ if setup is None:
+ setup = TlsTestSetup(env=self)
+ super().setup_httpd(setup=setup)
+
+ @property
+ def domain_a(self) -> str:
+ return self._domain_a
+
+ @property
+ def domain_b(self) -> str:
+ return self._domain_b
+
+ def tls_get(self, domain, paths: Union[str, List[str]], options: List[str] = None, no_stdout_list = False) -> ExecResult:
+ if isinstance(paths, str):
+ paths = [paths]
+ urls = [f"https://{domain}:{self.https_port}{path}" for path in paths]
+ return self.curl_raw(urls=urls, options=options, no_stdout_list=no_stdout_list)
+
+ def tls_get_json(self, domain: str, path: str, options=None):
+ r = self.tls_get(domain=domain, paths=path, options=options)
+ return r.json
+
+ def run_diff(self, fleft: str, fright: str) -> ExecResult:
+ return self.run(['diff', '-u', fleft, fright])
+
+ def openssl(self, args: List[str]) -> ExecResult:
+ return self.run(['openssl'] + args)
+
+ def openssl_client(self, domain, extra_args: List[str] = None) -> ExecResult:
+ args = ["s_client", "-CAfile", self.ca.cert_file, "-servername", domain,
+ "-connect", "localhost:{port}".format(
+ port=self.https_port
+ )]
+ if extra_args:
+ args.extend(extra_args)
+ args.extend([])
+ return self.openssl(args)
+
+ OPENSSL_SUPPORTED_PROTOCOLS = None
+
+ @staticmethod
+ def openssl_supports_tls_1_3() -> bool:
+ if TlsTestEnv.OPENSSL_SUPPORTED_PROTOCOLS is None:
+ env = TlsTestEnv()
+ r = env.openssl(args=["ciphers", "-v"])
+ protos = set()
+ ciphers = set()
+ for line in r.stdout.splitlines():
+ m = re.match(r'^(\S+)\s+(\S+)\s+(.*)$', line)
+ if m:
+ ciphers.add(m.group(1))
+ protos.add(m.group(2))
+ TlsTestEnv.OPENSSL_SUPPORTED_PROTOCOLS = protos
+ TlsTestEnv.OPENSSL_SUPPORTED_CIPHERS = ciphers
+ return "TLSv1.3" in TlsTestEnv.OPENSSL_SUPPORTED_PROTOCOLS
diff --git a/test/modules/tls/htdocs/a.mod-tls.test/index.json b/test/modules/tls/htdocs/a.mod-tls.test/index.json
new file mode 100644
index 0000000..ffc32cb
--- /dev/null
+++ b/test/modules/tls/htdocs/a.mod-tls.test/index.json
@@ -0,0 +1,3 @@
+{
+ "domain": "a.mod-tls.test"
+} \ No newline at end of file
diff --git a/test/modules/tls/htdocs/a.mod-tls.test/vars.py b/test/modules/tls/htdocs/a.mod-tls.test/vars.py
new file mode 100755
index 0000000..bd520e2
--- /dev/null
+++ b/test/modules/tls/htdocs/a.mod-tls.test/vars.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+import json
+import os, sys
+from urllib import parse
+import multipart # https://github.com/andrew-d/python-multipart (`apt install python3-multipart`)
+
+
+def get_request_params():
+ oforms = {}
+ ofiles = {}
+ if "REQUEST_URI" in os.environ:
+ qforms = parse.parse_qs(parse.urlsplit(os.environ["REQUEST_URI"]).query)
+ for name, values in qforms.items():
+ oforms[name] = values[0]
+ if "HTTP_CONTENT_TYPE" in os.environ:
+ ctype = os.environ["HTTP_CONTENT_TYPE"]
+ if ctype == "application/x-www-form-urlencoded":
+ qforms = parse.parse_qs(parse.urlsplit(sys.stdin.read()).query)
+ for name, values in qforms.items():
+ oforms[name] = values[0]
+ elif ctype.startswith("multipart/"):
+ def on_field(field):
+ oforms[field.field_name] = field.value
+ def on_file(file):
+ ofiles[field.field_name] = field.value
+ multipart.parse_form(headers={"Content-Type": ctype}, input_stream=sys.stdin.buffer, on_field=on_field, on_file=on_file)
+ return oforms, ofiles
+
+
+forms, files = get_request_params()
+
+jenc = json.JSONEncoder()
+
+def get_var(name: str, def_val: str = ""):
+ if name in os.environ:
+ return os.environ[name]
+ return def_val
+
+def get_json_var(name: str, def_val: str = ""):
+ var = get_var(name, def_val=def_val)
+ return jenc.encode(var)
+
+
+name = forms['name'] if 'name' in forms else None
+
+print("Content-Type: application/json\n")
+if name:
+ print(f"""{{ "{name}" : {get_json_var(name, '')}}}""")
+else:
+ print(f"""{{ "https" : {get_json_var('HTTPS', '')},
+ "host" : {get_json_var('SERVER_NAME', '')},
+ "protocol" : {get_json_var('SERVER_PROTOCOL', '')},
+ "ssl_protocol" : {get_json_var('SSL_PROTOCOL', '')},
+ "ssl_cipher" : {get_json_var('SSL_CIPHER', '')}
+}}""")
+
diff --git a/test/modules/tls/htdocs/b.mod-tls.test/dir1/vars.py b/test/modules/tls/htdocs/b.mod-tls.test/dir1/vars.py
new file mode 100755
index 0000000..b86a968
--- /dev/null
+++ b/test/modules/tls/htdocs/b.mod-tls.test/dir1/vars.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+import os
+
+def get_var(name: str, def_val: str = ""):
+ if name in os.environ:
+ return os.environ[name]
+ return def_val
+
+print("Content-Type: application/json")
+print()
+print("""{{ "https" : "{https}",
+ "host" : "{server_name}",
+ "protocol" : "{protocol}",
+ "ssl_protocol" : "{ssl_protocol}",
+ "ssl_cipher" : "{ssl_cipher}"
+}}""".format(
+ https=get_var('HTTPS', ''),
+ server_name=get_var('SERVER_NAME', ''),
+ protocol=get_var('SERVER_PROTOCOL', ''),
+ ssl_protocol=get_var('SSL_PROTOCOL', ''),
+ ssl_cipher=get_var('SSL_CIPHER', ''),
+))
+
diff --git a/test/modules/tls/htdocs/b.mod-tls.test/index.json b/test/modules/tls/htdocs/b.mod-tls.test/index.json
new file mode 100644
index 0000000..e5d3ccf
--- /dev/null
+++ b/test/modules/tls/htdocs/b.mod-tls.test/index.json
@@ -0,0 +1,3 @@
+{
+ "domain": "b.mod-tls.test"
+} \ No newline at end of file
diff --git a/test/modules/tls/htdocs/b.mod-tls.test/resp-jitter.py b/test/modules/tls/htdocs/b.mod-tls.test/resp-jitter.py
new file mode 100755
index 0000000..f7b1349
--- /dev/null
+++ b/test/modules/tls/htdocs/b.mod-tls.test/resp-jitter.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+import random
+import sys
+import time
+from datetime import timedelta
+
+random.seed()
+to_write = total_len = random.randint(1, 10*1024*1024)
+
+sys.stdout.write("Content-Type: application/octet-stream\n")
+sys.stdout.write(f"Content-Length: {total_len}\n")
+sys.stdout.write("\n")
+sys.stdout.flush()
+
+while to_write > 0:
+ len = random.randint(1, 1024*1024)
+ len = min(len, to_write)
+ sys.stdout.buffer.write(random.randbytes(len))
+ to_write -= len
+ delay = timedelta(seconds=random.uniform(0.0, 0.5))
+ time.sleep(delay.total_seconds())
+sys.stdout.flush()
+
diff --git a/test/modules/tls/htdocs/b.mod-tls.test/vars.py b/test/modules/tls/htdocs/b.mod-tls.test/vars.py
new file mode 100755
index 0000000..bd520e2
--- /dev/null
+++ b/test/modules/tls/htdocs/b.mod-tls.test/vars.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+import json
+import os, sys
+from urllib import parse
+import multipart # https://github.com/andrew-d/python-multipart (`apt install python3-multipart`)
+
+
+def get_request_params():
+ oforms = {}
+ ofiles = {}
+ if "REQUEST_URI" in os.environ:
+ qforms = parse.parse_qs(parse.urlsplit(os.environ["REQUEST_URI"]).query)
+ for name, values in qforms.items():
+ oforms[name] = values[0]
+ if "HTTP_CONTENT_TYPE" in os.environ:
+ ctype = os.environ["HTTP_CONTENT_TYPE"]
+ if ctype == "application/x-www-form-urlencoded":
+ qforms = parse.parse_qs(parse.urlsplit(sys.stdin.read()).query)
+ for name, values in qforms.items():
+ oforms[name] = values[0]
+ elif ctype.startswith("multipart/"):
+ def on_field(field):
+ oforms[field.field_name] = field.value
+ def on_file(file):
+ ofiles[field.field_name] = field.value
+ multipart.parse_form(headers={"Content-Type": ctype}, input_stream=sys.stdin.buffer, on_field=on_field, on_file=on_file)
+ return oforms, ofiles
+
+
+forms, files = get_request_params()
+
+jenc = json.JSONEncoder()
+
+def get_var(name: str, def_val: str = ""):
+ if name in os.environ:
+ return os.environ[name]
+ return def_val
+
+def get_json_var(name: str, def_val: str = ""):
+ var = get_var(name, def_val=def_val)
+ return jenc.encode(var)
+
+
+name = forms['name'] if 'name' in forms else None
+
+print("Content-Type: application/json\n")
+if name:
+ print(f"""{{ "{name}" : {get_json_var(name, '')}}}""")
+else:
+ print(f"""{{ "https" : {get_json_var('HTTPS', '')},
+ "host" : {get_json_var('SERVER_NAME', '')},
+ "protocol" : {get_json_var('SERVER_PROTOCOL', '')},
+ "ssl_protocol" : {get_json_var('SSL_PROTOCOL', '')},
+ "ssl_cipher" : {get_json_var('SSL_CIPHER', '')}
+}}""")
+
diff --git a/test/modules/tls/htdocs/index.html b/test/modules/tls/htdocs/index.html
new file mode 100644
index 0000000..3c07626
--- /dev/null
+++ b/test/modules/tls/htdocs/index.html
@@ -0,0 +1,9 @@
+<html>
+ <head>
+ <title>mod_h2 test site generic</title>
+ </head>
+ <body>
+ <h1>mod_h2 test site generic</h1>
+ </body>
+</html>
+
diff --git a/test/modules/tls/htdocs/index.json b/test/modules/tls/htdocs/index.json
new file mode 100644
index 0000000..6d456e0
--- /dev/null
+++ b/test/modules/tls/htdocs/index.json
@@ -0,0 +1,3 @@
+{
+ "domain": "localhost"
+} \ No newline at end of file
diff --git a/test/modules/tls/test_01_apache.py b/test/modules/tls/test_01_apache.py
new file mode 100644
index 0000000..cb6af6d
--- /dev/null
+++ b/test/modules/tls/test_01_apache.py
@@ -0,0 +1,14 @@
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestApache:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ TlsTestConf(env=env).install()
+ assert env.apache_restart() == 0
+
+ def test_tls_01_apache_http(self, env):
+ assert env.is_live(env.http_base_url)
diff --git a/test/modules/tls/test_02_conf.py b/test/modules/tls/test_02_conf.py
new file mode 100644
index 0000000..4d6aa60
--- /dev/null
+++ b/test/modules/tls/test_02_conf.py
@@ -0,0 +1,138 @@
+import os
+from datetime import timedelta
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestConf:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ TlsTestConf(env=env).install()
+ assert env.apache_restart() == 0
+
+ @pytest.fixture(autouse=True, scope='function')
+ def _function_scope(self, env):
+ if env.is_live(timeout=timedelta(milliseconds=100)):
+ assert env.apache_stop() == 0
+
+ def test_tls_02_conf_cert_args_missing(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSCertificate")
+ conf.install()
+ assert env.apache_fail() == 0
+
+ def test_tls_02_conf_cert_single_arg(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSCertificate cert.pem")
+ conf.install()
+ assert env.apache_fail() == 0
+
+ def test_tls_02_conf_cert_file_missing(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSCertificate cert.pem key.pem")
+ conf.install()
+ assert env.apache_fail() == 0
+
+ def test_tls_02_conf_cert_file_exist(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSCertificate test-02-cert.pem test-02-key.pem")
+ conf.install()
+ for name in ["test-02-cert.pem", "test-02-key.pem"]:
+ with open(os.path.join(env.server_dir, name), "w") as fd:
+ fd.write("")
+ assert env.apache_fail() == 0
+
+ def test_tls_02_conf_cert_listen_missing(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSEngine")
+ conf.install()
+ assert env.apache_fail() == 0
+
+ def test_tls_02_conf_cert_listen_wrong(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSEngine ^^^^^")
+ conf.install()
+ assert env.apache_fail() == 0
+
+ @pytest.mark.parametrize("listen", [
+ "443",
+ "129.168.178.188:443",
+ "[::]:443",
+ ])
+ def test_tls_02_conf_cert_listen_valid(self, env, listen: str):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSEngine {listen}".format(listen=listen))
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_tls_02_conf_cert_listen_cert(self, env):
+ domain = env.domain_a
+ conf = TlsTestConf(env=env)
+ conf.add_tls_vhosts(domains=[domain])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_tls_02_conf_proto_wrong(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSProtocol wrong")
+ conf.install()
+ assert env.apache_fail() == 0
+
+ @pytest.mark.parametrize("proto", [
+ "default",
+ "TLSv1.2+",
+ "TLSv1.3+",
+ "TLSv0x0303+",
+ ])
+ def test_tls_02_conf_proto_valid(self, env, proto):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSProtocol {proto}".format(proto=proto))
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_tls_02_conf_honor_wrong(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSHonorClientOrder wrong")
+ conf.install()
+ assert env.apache_fail() == 0
+
+ @pytest.mark.parametrize("honor", [
+ "on",
+ "OfF",
+ ])
+ def test_tls_02_conf_honor_valid(self, env, honor: str):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSHonorClientOrder {honor}".format(honor=honor))
+ conf.install()
+ assert env.apache_restart() == 0
+
+ @pytest.mark.parametrize("cipher", [
+ "default",
+ "TLS13_AES_128_GCM_SHA256:TLS13_AES_256_GCM_SHA384:TLS13_CHACHA20_POLY1305_SHA256",
+ "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:"
+ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:"
+ "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
+ """TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 \\
+ TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\\
+ TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"""
+ ])
+ def test_tls_02_conf_cipher_valid(self, env, cipher):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSCiphersPrefer {cipher}".format(cipher=cipher))
+ conf.install()
+ assert env.apache_restart() == 0
+
+ @pytest.mark.parametrize("cipher", [
+ "wrong",
+ "YOLO",
+ "TLS_NULL_WITH_NULL_NULLX", # not supported
+ "TLS_DHE_RSA_WITH_AES128_GCM_SHA256", # not supported
+ ])
+ def test_tls_02_conf_cipher_wrong(self, env, cipher):
+ conf = TlsTestConf(env=env)
+ conf.add("TLSCiphersPrefer {cipher}".format(cipher=cipher))
+ conf.install()
+ assert env.apache_fail() == 0
diff --git a/test/modules/tls/test_03_sni.py b/test/modules/tls/test_03_sni.py
new file mode 100644
index 0000000..cf421c0
--- /dev/null
+++ b/test/modules/tls/test_03_sni.py
@@ -0,0 +1,71 @@
+from datetime import timedelta
+
+import pytest
+
+from .conf import TlsTestConf
+from .env import TlsTestEnv
+
+
+class TestSni:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ @pytest.fixture(autouse=True, scope='function')
+ def _function_scope(self, env):
+ pass
+
+ def test_tls_03_sni_get_a(self, env):
+ # do we see the correct json for the domain_a?
+ data = env.tls_get_json(env.domain_a, "/index.json")
+ assert data == {'domain': env.domain_a}
+
+ def test_tls_03_sni_get_b(self, env):
+ # do we see the correct json for the domain_a?
+ data = env.tls_get_json(env.domain_b, "/index.json")
+ assert data == {'domain': env.domain_b}
+
+ def test_tls_03_sni_unknown(self, env):
+ # connection will be denied as cert does not cover this domain
+ domain_unknown = "unknown.test"
+ r = env.tls_get(domain_unknown, "/index.json")
+ assert r.exit_code != 0
+
+ def test_tls_03_sni_request_other_same_config(self, env):
+ # do we see the first vhost response for another domain with different certs?
+ r = env.tls_get(env.domain_a, "/index.json", options=[
+ "-vvvv", "--header", "Host: {0}".format(env.domain_b)
+ ])
+ # request is marked as misdirected
+ assert r.exit_code == 0
+ assert r.json is None
+ assert r.response['status'] == 421
+
+ def test_tls_03_sni_request_other_other_honor(self, env):
+ # do we see the first vhost response for an unknown domain?
+ conf = TlsTestConf(env=env, extras={
+ env.domain_a: "TLSProtocol TLSv1.2+",
+ env.domain_b: "TLSProtocol TLSv1.3+"
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ r = env.tls_get(env.domain_a, "/index.json", options=[
+ "-vvvv", "--tls-max", "1.2", "--header", "Host: {0}".format(env.domain_b)
+ ])
+ # request denied
+ assert r.exit_code == 0
+ assert r.json is None
+
+ @pytest.mark.skip('openssl behaviour changed on ventura, unreliable')
+ def test_tls_03_sni_bad_hostname(self, env):
+ # curl checks hostnames we give it, but the openssl client
+ # does not. Good for us, since we need to test it.
+ r = env.openssl(["s_client", "-connect",
+ "localhost:{0}".format(env.https_port),
+ "-servername", b'x\x2f.y'.decode()])
+ assert r.exit_code == 1, r.stderr
diff --git a/test/modules/tls/test_04_get.py b/test/modules/tls/test_04_get.py
new file mode 100644
index 0000000..6944381
--- /dev/null
+++ b/test/modules/tls/test_04_get.py
@@ -0,0 +1,67 @@
+import os
+import time
+from datetime import timedelta
+
+import pytest
+
+from .env import TlsTestEnv
+from .conf import TlsTestConf
+
+
+def mk_text_file(fpath: str, lines: int):
+ t110 = 11 * "0123456789"
+ with open(fpath, "w") as fd:
+ for i in range(lines):
+ fd.write("{0:015d}: ".format(i)) # total 128 bytes per line
+ fd.write(t110)
+ fd.write("\n")
+
+
+class TestGet:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ docs_a = os.path.join(env.server_docs_dir, env.domain_a)
+ mk_text_file(os.path.join(docs_a, "1k.txt"), 8)
+ mk_text_file(os.path.join(docs_a, "10k.txt"), 80)
+ mk_text_file(os.path.join(docs_a, "100k.txt"), 800)
+ mk_text_file(os.path.join(docs_a, "1m.txt"), 8000)
+ mk_text_file(os.path.join(docs_a, "10m.txt"), 80000)
+ assert env.apache_restart() == 0
+
+ @pytest.mark.parametrize("fname, flen", [
+ ("1k.txt", 1024),
+ ("10k.txt", 10*1024),
+ ("100k.txt", 100 * 1024),
+ ("1m.txt", 1000 * 1024),
+ ("10m.txt", 10000 * 1024),
+ ])
+ def test_tls_04_get(self, env, fname, flen):
+ # do we see the correct json for the domain_a?
+ docs_a = os.path.join(env.server_docs_dir, env.domain_a)
+ r = env.tls_get(env.domain_a, "/{0}".format(fname))
+ assert r.exit_code == 0
+ assert len(r.stdout) == flen
+ pref = os.path.join(docs_a, fname)
+ pout = os.path.join(docs_a, "{0}.out".format(fname))
+ with open(pout, 'w') as fd:
+ fd.write(r.stdout)
+ dr = env.run_diff(pref, pout)
+ assert dr.exit_code == 0, "differences found:\n{0}".format(dr.stdout)
+
+ @pytest.mark.parametrize("fname, flen", [
+ ("1k.txt", 1024),
+ ])
+ def test_tls_04_double_get(self, env, fname, flen):
+ # we'd like to check that we can do >1 requests on the same connection
+ # however curl hides that from us, unless we analyze its verbose output
+ docs_a = os.path.join(env.server_docs_dir, env.domain_a)
+ r = env.tls_get(env.domain_a, no_stdout_list=True, paths=[
+ "/{0}".format(fname),
+ "/{0}".format(fname)
+ ])
+ assert r.exit_code == 0
+ assert len(r.stdout) == 2*flen
diff --git a/test/modules/tls/test_05_proto.py b/test/modules/tls/test_05_proto.py
new file mode 100644
index 0000000..d874a90
--- /dev/null
+++ b/test/modules/tls/test_05_proto.py
@@ -0,0 +1,64 @@
+import time
+from datetime import timedelta
+import socket
+from threading import Thread
+
+import pytest
+
+from .conf import TlsTestConf
+from .env import TlsTestEnv
+
+
+class TestProto:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env, extras={
+ env.domain_a: "TLSProtocol TLSv1.3+",
+ env.domain_b: [
+ "# the commonly used name",
+ "TLSProtocol TLSv1.2+",
+ "# the numeric one (yes, this is 1.2)",
+ "TLSProtocol TLSv0x0303+",
+ ],
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ @pytest.fixture(autouse=True, scope='function')
+ def _function_scope(self, env):
+ pass
+
+ def test_tls_05_proto_1_2(self, env):
+ r = env.tls_get(env.domain_b, "/index.json", options=["--tlsv1.2"])
+ assert r.exit_code == 0, r.stderr
+
+ @pytest.mark.skip('curl does not have TLSv1.3 on all platforms')
+ def test_tls_05_proto_1_3(self, env):
+ r = env.tls_get(env.domain_a, "/index.json", options=["--tlsv1.3", '-v'])
+ if True: # testing TlsTestEnv.curl_supports_tls_1_3() is unreliable (curl should support TLS1.3 nowadays..)
+ assert r.exit_code == 0, f'{r}'
+ else:
+ assert r.exit_code == 4, f'{r}'
+
+ def test_tls_05_proto_close(self, env):
+ s = socket.create_connection(('localhost', env.https_port))
+ time.sleep(0.1)
+ s.close()
+
+ def test_tls_05_proto_ssl_close(self, env):
+ conf = TlsTestConf(env=env, extras={
+ 'base': "LogLevel ssl:debug",
+ env.domain_a: "SSLProtocol TLSv1.3",
+ env.domain_b: "SSLProtocol TLSv1.2",
+ })
+ for d in [env.domain_a, env.domain_b]:
+ conf.add_vhost(domains=[d], port=env.https_port)
+ conf.install()
+ assert env.apache_restart() == 0
+ s = socket.create_connection(('localhost', env.https_port))
+ time.sleep(0.1)
+ s.close()
+
+
diff --git a/test/modules/tls/test_06_ciphers.py b/test/modules/tls/test_06_ciphers.py
new file mode 100644
index 0000000..2e60bdd
--- /dev/null
+++ b/test/modules/tls/test_06_ciphers.py
@@ -0,0 +1,209 @@
+import re
+from datetime import timedelta
+
+import pytest
+
+from .env import TlsTestEnv
+from .conf import TlsTestConf
+
+
+class TestCiphers:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env, extras={
+ 'base': "TLSHonorClientOrder off",
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ @pytest.fixture(autouse=True, scope='function')
+ def _function_scope(self, env):
+ pass
+
+ def _get_protocol_cipher(self, output: str):
+ protocol = None
+ cipher = None
+ for line in output.splitlines():
+ m = re.match(r'^\s+Protocol\s*:\s*(\S+)$', line)
+ if m:
+ protocol = m.group(1)
+ continue
+ m = re.match(r'^\s+Cipher\s*:\s*(\S+)$', line)
+ if m:
+ cipher = m.group(1)
+ return protocol, cipher
+
+ def test_tls_06_ciphers_ecdsa(self, env):
+ ecdsa_1_2 = [c for c in env.RUSTLS_CIPHERS
+ if c.max_version == 1.2 and c.flavour == 'ECDSA'][0]
+ # client speaks only this cipher, see that it gets it
+ r = env.openssl_client(env.domain_b, extra_args=[
+ "-cipher", ecdsa_1_2.openssl_name, "-tls1_2"
+ ])
+ protocol, cipher = self._get_protocol_cipher(r.stdout)
+ assert protocol == "TLSv1.2", r.stdout
+ assert cipher == ecdsa_1_2.openssl_name, r.stdout
+
+ def test_tls_06_ciphers_rsa(self, env):
+ rsa_1_2 = [c for c in env.RUSTLS_CIPHERS
+ if c.max_version == 1.2 and c.flavour == 'RSA'][0]
+ # client speaks only this cipher, see that it gets it
+ r = env.openssl_client(env.domain_b, extra_args=[
+ "-cipher", rsa_1_2.openssl_name, "-tls1_2"
+ ])
+ protocol, cipher = self._get_protocol_cipher(r.stdout)
+ assert protocol == "TLSv1.2", r.stdout
+ assert cipher == rsa_1_2.openssl_name, r.stdout
+
+ @pytest.mark.parametrize("cipher", [
+ c for c in TlsTestEnv.RUSTLS_CIPHERS if c.max_version == 1.2 and c.flavour == 'ECDSA'
+ ], ids=[
+ c.name for c in TlsTestEnv.RUSTLS_CIPHERS if c.max_version == 1.2 and c.flavour == 'ECDSA'
+ ])
+ def test_tls_06_ciphers_server_prefer_ecdsa(self, env, cipher):
+ # Select a ECSDA ciphers as preference and suppress all RSA ciphers.
+ # The last is not strictly necessary since rustls prefers ECSDA anyway
+ suppress_names = [c.name for c in env.RUSTLS_CIPHERS
+ if c.max_version == 1.2 and c.flavour == 'RSA']
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: [
+ "TLSHonorClientOrder off",
+ f"TLSCiphersPrefer {cipher.name}",
+ f"TLSCiphersSuppress {':'.join(suppress_names)}",
+ ]
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ r = env.openssl_client(env.domain_b, extra_args=["-tls1_2"])
+ client_proto, client_cipher = self._get_protocol_cipher(r.stdout)
+ assert client_proto == "TLSv1.2", r.stdout
+ assert client_cipher == cipher.openssl_name, r.stdout
+
+ @pytest.mark.skip(reason="Wrong certified key selected by rustls")
+ # see <https://github.com/rustls/rustls-ffi/issues/236>
+ @pytest.mark.parametrize("cipher", [
+ c for c in TlsTestEnv.RUSTLS_CIPHERS if c.max_version == 1.2 and c.flavour == 'RSA'
+ ], ids=[
+ c.name for c in TlsTestEnv.RUSTLS_CIPHERS if c.max_version == 1.2 and c.flavour == 'RSA'
+ ])
+ def test_tls_06_ciphers_server_prefer_rsa(self, env, cipher):
+ # Select a RSA ciphers as preference and suppress all ECDSA ciphers.
+ # The last is necessary since rustls prefers ECSDA and openssl leaks that it can.
+ suppress_names = [c.name for c in env.RUSTLS_CIPHERS
+ if c.max_version == 1.2 and c.flavour == 'ECDSA']
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: [
+ "TLSHonorClientOrder off",
+ f"TLSCiphersPrefer {cipher.name}",
+ f"TLSCiphersSuppress {':'.join(suppress_names)}",
+ ]
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ r = env.openssl_client(env.domain_b, extra_args=["-tls1_2"])
+ client_proto, client_cipher = self._get_protocol_cipher(r.stdout)
+ assert client_proto == "TLSv1.2", r.stdout
+ assert client_cipher == cipher.openssl_name, r.stdout
+
+ @pytest.mark.skip(reason="Wrong certified key selected by rustls")
+ # see <https://github.com/rustls/rustls-ffi/issues/236>
+ @pytest.mark.parametrize("cipher", [
+ c for c in TlsTestEnv.RUSTLS_CIPHERS if c.max_version == 1.2 and c.flavour == 'RSA'
+ ], ids=[
+ c.openssl_name for c in TlsTestEnv.RUSTLS_CIPHERS if c.max_version == 1.2 and c.flavour == 'RSA'
+ ])
+ def test_tls_06_ciphers_server_prefer_rsa_alias(self, env, cipher):
+ # same as above, but using openssl names for ciphers
+ suppress_names = [c.openssl_name for c in env.RUSTLS_CIPHERS
+ if c.max_version == 1.2 and c.flavour == 'ECDSA']
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: [
+ "TLSHonorClientOrder off",
+ f"TLSCiphersPrefer {cipher.openssl_name}",
+ f"TLSCiphersSuppress {':'.join(suppress_names)}",
+ ]
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ r = env.openssl_client(env.domain_b, extra_args=["-tls1_2"])
+ client_proto, client_cipher = self._get_protocol_cipher(r.stdout)
+ assert client_proto == "TLSv1.2", r.stdout
+ assert client_cipher == cipher.openssl_name, r.stdout
+
+ @pytest.mark.skip(reason="Wrong certified key selected by rustls")
+ # see <https://github.com/rustls/rustls-ffi/issues/236>
+ @pytest.mark.parametrize("cipher", [
+ c for c in TlsTestEnv.RUSTLS_CIPHERS if c.max_version == 1.2 and c.flavour == 'RSA'
+ ], ids=[
+ c.id_name for c in TlsTestEnv.RUSTLS_CIPHERS if c.max_version == 1.2 and c.flavour == 'RSA'
+ ])
+ def test_tls_06_ciphers_server_prefer_rsa_id(self, env, cipher):
+ # same as above, but using openssl names for ciphers
+ suppress_names = [c.id_name for c in env.RUSTLS_CIPHERS
+ if c.max_version == 1.2 and c.flavour == 'ECDSA']
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: [
+ "TLSHonorClientOrder off",
+ f"TLSCiphersPrefer {cipher.id_name}",
+ f"TLSCiphersSuppress {':'.join(suppress_names)}",
+ ]
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ r = env.openssl_client(env.domain_b, extra_args=["-tls1_2"])
+ client_proto, client_cipher = self._get_protocol_cipher(r.stdout)
+ assert client_proto == "TLSv1.2", r.stdout
+ assert client_cipher == cipher.openssl_name, r.stdout
+
+ def test_tls_06_ciphers_pref_unknown(self, env):
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: "TLSCiphersPrefer TLS_MY_SUPER_CIPHER:SSL_WHAT_NOT"
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() != 0
+ # get a working config again, so that subsequent test cases do not stumble
+ conf = TlsTestConf(env=env)
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ env.apache_restart()
+
+ def test_tls_06_ciphers_pref_unsupported(self, env):
+ # a warning on preferring a known, but not supported cipher
+ env.httpd_error_log.ignore_recent()
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: "TLSCiphersPrefer TLS_NULL_WITH_NULL_NULL"
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ (errors, warnings) = env.httpd_error_log.get_recent_count()
+ assert errors == 0
+ assert warnings == 2 # once on dry run, once on start
+
+ def test_tls_06_ciphers_supp_unknown(self, env):
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: "TLSCiphersSuppress TLS_MY_SUPER_CIPHER:SSL_WHAT_NOT"
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() != 0
+
+ def test_tls_06_ciphers_supp_unsupported(self, env):
+ # no warnings on suppressing known, but not supported ciphers
+ env.httpd_error_log.ignore_recent()
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: "TLSCiphersSuppress TLS_NULL_WITH_NULL_NULL"
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ (errors, warnings) = env.httpd_error_log.get_recent_count()
+ assert errors == 0
+ assert warnings == 0
diff --git a/test/modules/tls/test_07_alpn.py b/test/modules/tls/test_07_alpn.py
new file mode 100644
index 0000000..06aff3c
--- /dev/null
+++ b/test/modules/tls/test_07_alpn.py
@@ -0,0 +1,43 @@
+import re
+from datetime import timedelta
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestAlpn:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: "Protocols h2 http/1.1"
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ @pytest.fixture(autouse=True, scope='function')
+ def _function_scope(self, env):
+ pass
+
+ def _get_protocol(self, output: str):
+ for line in output.splitlines():
+ m = re.match(r'^\*\s+ALPN[:,] server accepted (to use\s+)?(.*)$', line)
+ if m:
+ return m.group(2)
+ return None
+
+ def test_tls_07_alpn_get_a(self, env):
+ # do we see the correct json for the domain_a?
+ r = env.tls_get(env.domain_a, "/index.json", options=["-vvvvvv", "--http1.1"])
+ assert r.exit_code == 0, r.stderr
+ protocol = self._get_protocol(r.stderr)
+ assert protocol == "http/1.1", r.stderr
+
+ def test_tls_07_alpn_get_b(self, env):
+ # do we see the correct json for the domain_a?
+ r = env.tls_get(env.domain_b, "/index.json", options=["-vvvvvv"])
+ assert r.exit_code == 0, r.stderr
+ protocol = self._get_protocol(r.stderr)
+ assert protocol == "h2", r.stderr
diff --git a/test/modules/tls/test_08_vars.py b/test/modules/tls/test_08_vars.py
new file mode 100644
index 0000000..f1bd9b4
--- /dev/null
+++ b/test/modules/tls/test_08_vars.py
@@ -0,0 +1,60 @@
+import re
+
+import pytest
+
+from .conf import TlsTestConf
+from .env import TlsTestEnv
+
+
+class TestVars:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env, extras={
+ 'base': [
+ "TLSHonorClientOrder off",
+ "TLSOptions +StdEnvVars",
+ ]
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_tls_08_vars_root(self, env):
+ # in domain_b root, the StdEnvVars is switch on
+ exp_proto = "TLSv1.2"
+ exp_cipher = "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
+ options = [ '--tls-max', '1.2']
+ r = env.tls_get(env.domain_b, "/vars.py", options=options)
+ assert r.exit_code == 0, r.stderr
+ assert r.json == {
+ 'https': 'on',
+ 'host': 'b.mod-tls.test',
+ 'protocol': 'HTTP/1.1',
+ 'ssl_protocol': exp_proto,
+ # this will vary by client potentially
+ 'ssl_cipher': exp_cipher,
+ }
+
+ @pytest.mark.parametrize("name, value", [
+ ("SERVER_NAME", "b.mod-tls.test"),
+ ("SSL_SESSION_RESUMED", "Initial"),
+ ("SSL_SECURE_RENEG", "false"),
+ ("SSL_COMPRESS_METHOD", "NULL"),
+ ("SSL_CIPHER_EXPORT", "false"),
+ ("SSL_CLIENT_VERIFY", "NONE"),
+ ])
+ def test_tls_08_vars_const(self, env, name: str, value: str):
+ r = env.tls_get(env.domain_b, f"/vars.py?name={name}")
+ assert r.exit_code == 0, r.stderr
+ assert r.json == {name: value}, r.stdout
+
+ @pytest.mark.parametrize("name, pattern", [
+ ("SSL_VERSION_INTERFACE", r'mod_tls/\d+\.\d+\.\d+'),
+ ("SSL_VERSION_LIBRARY", r'rustls-ffi/\d+\.\d+\.\d+/rustls/\d+\.\d+\.\d+'),
+ ])
+ def test_tls_08_vars_match(self, env, name: str, pattern: str):
+ r = env.tls_get(env.domain_b, f"/vars.py?name={name}")
+ assert r.exit_code == 0, r.stderr
+ assert name in r.json
+ assert re.match(pattern, r.json[name]), r.json
diff --git a/test/modules/tls/test_09_timeout.py b/test/modules/tls/test_09_timeout.py
new file mode 100644
index 0000000..70cc894
--- /dev/null
+++ b/test/modules/tls/test_09_timeout.py
@@ -0,0 +1,43 @@
+import socket
+from datetime import timedelta
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestTimeout:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env, extras={
+ 'base': "RequestReadTimeout handshake=1",
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ @pytest.fixture(autouse=True, scope='function')
+ def _function_scope(self, env):
+ pass
+
+ def test_tls_09_timeout_handshake(self, env):
+ # in domain_b root, the StdEnvVars is switch on
+ s = socket.create_connection(('localhost', env.https_port))
+ s.send(b'1234')
+ s.settimeout(0.0)
+ try:
+ s.recv(1024)
+ assert False, "able to recv() on a TLS connection before we sent a hello"
+ except BlockingIOError:
+ pass
+ s.settimeout(3.0)
+ try:
+ while True:
+ buf = s.recv(1024)
+ if not buf:
+ break
+ print("recv() -> {0}".format(buf))
+ except (socket.timeout, BlockingIOError):
+ assert False, "socket not closed as handshake timeout should trigger"
+ s.close()
diff --git a/test/modules/tls/test_10_session_id.py b/test/modules/tls/test_10_session_id.py
new file mode 100644
index 0000000..848bc1a
--- /dev/null
+++ b/test/modules/tls/test_10_session_id.py
@@ -0,0 +1,50 @@
+import re
+from typing import List
+
+import pytest
+
+from pyhttpd.result import ExecResult
+from .env import TlsTestEnv
+from .conf import TlsTestConf
+
+
+class TestSessionID:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env)
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def find_openssl_session_ids(self, r: ExecResult) -> List[str]:
+ ids = []
+ for line in r.stdout.splitlines():
+ m = re.match(r'^\s*Session-ID: (\S+)$', line)
+ if m:
+ ids.append(m.group(1))
+ return ids
+
+ def test_tls_10_session_id_12(self, env):
+ r = env.openssl_client(env.domain_b, extra_args=[
+ "-reconnect", "-tls1_2"
+ ])
+ session_ids = self.find_openssl_session_ids(r)
+ assert 1 < len(session_ids), "expected several session-ids: {0}, stderr={1}".format(
+ session_ids, r.stderr
+ )
+ assert 1 == len(set(session_ids)), "sesion-ids should all be the same: {0}".format(session_ids)
+
+ @pytest.mark.skipif(True or not TlsTestEnv.openssl_supports_tls_1_3(),
+ reason="openssl TLSv1.3 session storage test incomplete")
+ def test_tls_10_session_id_13(self, env):
+ r = env.openssl_client(env.domain_b, extra_args=[
+ "-reconnect", "-tls1_3"
+ ])
+ # openssl -reconnect closes connection immediately after the handhshake, so
+ # the Session data in TLSv1.3 is not seen and not found in its output.
+ # FIXME: how to check session data with TLSv1.3?
+ session_ids = self.find_openssl_session_ids(r)
+ assert 0 == len(session_ids), "expected no session-ids: {0}, stderr={1}".format(
+ session_ids, r.stdout
+ )
diff --git a/test/modules/tls/test_11_md.py b/test/modules/tls/test_11_md.py
new file mode 100644
index 0000000..9d733db
--- /dev/null
+++ b/test/modules/tls/test_11_md.py
@@ -0,0 +1,37 @@
+import time
+from datetime import timedelta
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestMD:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env, extras={
+ 'base': "LogLevel md:trace4"
+ })
+ conf.add_md_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_tls_11_get_a(self, env):
+ # do we see the correct json for the domain_a?
+ data = env.tls_get_json(env.domain_a, "/index.json")
+ assert data == {'domain': env.domain_a}
+
+ def test_tls_11_get_b(self, env):
+ # do we see the correct json for the domain_a?
+ data = env.tls_get_json(env.domain_b, "/index.json")
+ assert data == {'domain': env.domain_b}
+
+ def test_tls_11_get_base(self, env):
+ # give the base server domain_a and lookup its index.json
+ conf = TlsTestConf(env=env)
+ conf.add_md_base(domain=env.domain_a)
+ conf.install()
+ assert env.apache_restart() == 0
+ data = env.tls_get_json(env.domain_a, "/index.json")
+ assert data == {'domain': 'localhost'}
diff --git a/test/modules/tls/test_12_cauth.py b/test/modules/tls/test_12_cauth.py
new file mode 100644
index 0000000..1411609
--- /dev/null
+++ b/test/modules/tls/test_12_cauth.py
@@ -0,0 +1,235 @@
+import os
+from datetime import timedelta
+from typing import Optional
+
+import pytest
+
+from pyhttpd.certs import Credentials
+from .conf import TlsTestConf
+
+
+@pytest.fixture
+def clients_x(env):
+ return env.ca.get_first("clientsX")
+
+
+@pytest.fixture
+def clients_y(env):
+ return env.ca.get_first("clientsY")
+
+
+@pytest.fixture
+def cax_file(clients_x):
+ return os.path.join(os.path.dirname(clients_x.cert_file), "clientX-ca.pem")
+
+
+@pytest.mark.skip(reason="client certs disabled")
+class TestTLS:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env, clients_x, cax_file):
+ with open(cax_file, 'w') as fd:
+ fd.write("".join(open(clients_x.cert_file).readlines()))
+ fd.write("".join(open(env.ca.cert_file).readlines()))
+
+ @pytest.fixture(autouse=True, scope='function')
+ def _function_scope(self, env):
+ if env.is_live(timeout=timedelta(milliseconds=100)):
+ assert env.apache_stop() == 0
+
+ def get_ssl_var(self, env, domain: str, cert: Optional[Credentials], name: str):
+ r = env.tls_get(domain, f"/vars.py?name={name}", options=[
+ "--cert", cert.cert_file
+ ] if cert else [])
+ assert r.exit_code == 0, r.stderr
+ assert r.json, r.stderr + r.stdout
+ return r.json[name] if name in r.json else None
+
+ def test_tls_12_set_ca_non_existing(self, env):
+ conf = TlsTestConf(env=env, extras={
+ env.domain_a: "TLSClientCA xxx"
+ })
+ conf.add_md_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 1
+
+ def test_tls_12_set_ca_existing(self, env, cax_file):
+ conf = TlsTestConf(env=env, extras={
+ env.domain_a: f"TLSClientCA {cax_file}"
+ })
+ conf.add_md_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_tls_12_set_auth_no_ca(self, env):
+ conf = TlsTestConf(env=env, extras={
+ env.domain_a: "TLSClientCertificate required"
+ })
+ conf.add_md_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ # will fail bc lacking clien CA
+ assert env.apache_restart() == 1
+
+ def test_tls_12_auth_option_std(self, env, cax_file, clients_x):
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: [
+ f"TLSClientCertificate required",
+ f"TLSClientCA {cax_file}",
+ "# TODO: TLSUserName SSL_CLIENT_S_DN_CN",
+ "TLSOptions +StdEnvVars",
+ ]
+ })
+ conf.add_md_vhosts(domains=[env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ # should be denied
+ r = env.tls_get(domain=env.domain_b, paths="/index.json")
+ assert r.exit_code != 0, r.stdout
+ # should work
+ ccert = clients_x.get_first("user1")
+ data = env.tls_get_json(env.domain_b, "/index.json", options=[
+ "--cert", ccert.cert_file
+ ])
+ assert data == {'domain': env.domain_b}
+ r = env.tls_get(env.domain_b, "/vars.py?name=SSL_CLIENT_S_DN_CN")
+ assert r.exit_code != 0, "should have been prevented"
+ val = self.get_ssl_var(env, env.domain_b, ccert, "SSL_CLIENT_S_DN_CN")
+ assert val == 'Not Implemented'
+ # TODO
+ # val = self.get_ssl_var(env, env.domain_b, ccert, "REMOTE_USER")
+ # assert val == 'Not Implemented'
+ # not set on StdEnvVars, needs option ExportCertData
+ val = self.get_ssl_var(env, env.domain_b, ccert, "SSL_CLIENT_CERT")
+ assert val == ""
+
+ def test_tls_12_auth_option_cert(self, env, test_ca, cax_file, clients_x):
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: [
+ "TLSClientCertificate required",
+ f"TLSClientCA {cax_file}",
+ "TLSOptions Defaults +ExportCertData",
+ ]
+ })
+ conf.add_md_vhosts(domains=[env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ ccert = clients_x.get_first("user1")
+ val = self.get_ssl_var(env, env.domain_b, ccert, "SSL_CLIENT_CERT")
+ assert val == ccert.cert_pem.decode()
+ # no chain should be present
+ val = self.get_ssl_var(env, env.domain_b, ccert, "SSL_CLIENT_CHAIN_0")
+ assert val == ''
+ val = self.get_ssl_var(env, env.domain_b, ccert, "SSL_SERVER_CERT")
+ assert val
+ server_certs = test_ca.get_credentials_for_name(env.domain_b)
+ assert val in [c.cert_pem.decode() for c in server_certs]
+
+ def test_tls_12_auth_ssl_optional(self, env, cax_file, clients_x):
+ domain = env.domain_b
+ conf = TlsTestConf(env=env, extras={
+ domain: [
+ "SSLVerifyClient optional",
+ "SSLVerifyDepth 2",
+ "SSLOptions +StdEnvVars +ExportCertData",
+ f"SSLCACertificateFile {cax_file}",
+ "SSLUserName SSL_CLIENT_S_DN",
+ ]
+ })
+ conf.add_ssl_vhosts(domains=[domain])
+ conf.install()
+ assert env.apache_restart() == 0
+ # should work either way
+ data = env.tls_get_json(domain, "/index.json")
+ assert data == {'domain': domain}
+ # no client cert given, we expect the server variable to be empty
+ val = self.get_ssl_var(env, env.domain_b, None, "SSL_CLIENT_S_DN_CN")
+ assert val == ''
+ ccert = clients_x.get_first("user1")
+ data = env.tls_get_json(domain, "/index.json", options=[
+ "--cert", ccert.cert_file
+ ])
+ assert data == {'domain': domain}
+ val = self.get_ssl_var(env, env.domain_b, ccert, "SSL_CLIENT_S_DN_CN")
+ assert val == 'user1'
+ val = self.get_ssl_var(env, env.domain_b, ccert, "SSL_CLIENT_S_DN")
+ assert val == 'O=abetterinternet-mod_tls,OU=clientsX,CN=user1'
+ val = self.get_ssl_var(env, env.domain_b, ccert, "REMOTE_USER")
+ assert val == 'O=abetterinternet-mod_tls,OU=clientsX,CN=user1'
+ val = self.get_ssl_var(env, env.domain_b, ccert, "SSL_CLIENT_I_DN")
+ assert val == 'O=abetterinternet-mod_tls,OU=clientsX'
+ val = self.get_ssl_var(env, env.domain_b, ccert, "SSL_CLIENT_I_DN_CN")
+ assert val == ''
+ val = self.get_ssl_var(env, env.domain_b, ccert, "SSL_CLIENT_I_DN_OU")
+ assert val == 'clientsX'
+ val = self.get_ssl_var(env, env.domain_b, ccert, "SSL_CLIENT_CERT")
+ assert val == ccert.cert_pem.decode()
+
+ def test_tls_12_auth_optional(self, env, cax_file, clients_x):
+ domain = env.domain_b
+ conf = TlsTestConf(env=env, extras={
+ domain: [
+ "TLSClientCertificate optional",
+ f"TLSClientCA {cax_file}",
+ ]
+ })
+ conf.add_md_vhosts(domains=[domain])
+ conf.install()
+ assert env.apache_restart() == 0
+ # should work either way
+ data = env.tls_get_json(domain, "/index.json")
+ assert data == {'domain': domain}
+ # no client cert given, we expect the server variable to be empty
+ r = env.tls_get(domain, "/vars.py?name=SSL_CLIENT_S_DN_CN")
+ assert r.exit_code == 0, r.stderr
+ assert r.json == {
+ 'SSL_CLIENT_S_DN_CN': '',
+ }, r.stdout
+ data = env.tls_get_json(domain, "/index.json", options=[
+ "--cert", clients_x.get_first("user1").cert_file
+ ])
+ assert data == {'domain': domain}
+ r = env.tls_get(domain, "/vars.py?name=SSL_CLIENT_S_DN_CN", options=[
+ "--cert", clients_x.get_first("user1").cert_file
+ ])
+ # with client cert, we expect the server variable to show? Do we?
+ assert r.exit_code == 0, r.stderr
+ assert r.json == {
+ 'SSL_CLIENT_S_DN_CN': 'Not Implemented',
+ }, r.stdout
+
+ def test_tls_12_auth_expired(self, env, cax_file, clients_x):
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: [
+ "TLSClientCertificate required",
+ f"TLSClientCA {cax_file}",
+ ]
+ })
+ conf.add_md_vhosts(domains=[env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ # should not work
+ r = env.tls_get(domain=env.domain_b, paths="/index.json", options=[
+ "--cert", clients_x.get_first("user_expired").cert_file
+ ])
+ assert r.exit_code != 0
+
+ def test_tls_12_auth_other_ca(self, env, cax_file, clients_y):
+ conf = TlsTestConf(env=env, extras={
+ env.domain_b: [
+ "TLSClientCertificate required",
+ f"TLSClientCA {cax_file}",
+ ]
+ })
+ conf.add_md_vhosts(domains=[env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+ # should not work
+ r = env.tls_get(domain=env.domain_b, paths="/index.json", options=[
+ "--cert", clients_y.get_first("user1").cert_file
+ ])
+ assert r.exit_code != 0
+ # This will work, as the CA root is present in the CA file
+ r = env.tls_get(domain=env.domain_b, paths="/index.json", options=[
+ "--cert", env.ca.get_first("user1").cert_file
+ ])
+ assert r.exit_code == 0
diff --git a/test/modules/tls/test_13_proxy.py b/test/modules/tls/test_13_proxy.py
new file mode 100644
index 0000000..8bd305f
--- /dev/null
+++ b/test/modules/tls/test_13_proxy.py
@@ -0,0 +1,40 @@
+from datetime import timedelta
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestProxy:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env, extras={
+ 'base': "LogLevel proxy:trace1 proxy_http:trace1 ssl:trace1",
+ env.domain_b: [
+ "ProxyPreserveHost on",
+ f'ProxyPass "/proxy/" "http://127.0.0.1:{env.http_port}/"',
+ f'ProxyPassReverse "/proxy/" "http://{env.domain_b}:{env.http_port}"',
+ ]
+ })
+ # add vhosts a+b and a ssl proxy from a to b
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_tls_13_proxy_http_get(self, env):
+ data = env.tls_get_json(env.domain_b, "/proxy/index.json")
+ assert data == {'domain': env.domain_b}
+
+ @pytest.mark.parametrize("name, value", [
+ ("SERVER_NAME", "b.mod-tls.test"),
+ ("SSL_SESSION_RESUMED", ""),
+ ("SSL_SECURE_RENEG", ""),
+ ("SSL_COMPRESS_METHOD", ""),
+ ("SSL_CIPHER_EXPORT", ""),
+ ("SSL_CLIENT_VERIFY", ""),
+ ])
+ def test_tls_13_proxy_http_vars(self, env, name: str, value: str):
+ r = env.tls_get(env.domain_b, f"/proxy/vars.py?name={name}")
+ assert r.exit_code == 0, r.stderr
+ assert r.json == {name: value}, r.stdout
diff --git a/test/modules/tls/test_14_proxy_ssl.py b/test/modules/tls/test_14_proxy_ssl.py
new file mode 100644
index 0000000..79b2fb4
--- /dev/null
+++ b/test/modules/tls/test_14_proxy_ssl.py
@@ -0,0 +1,78 @@
+import re
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestProxySSL:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ # add vhosts a+b and a ssl proxy from a to b
+ conf = TlsTestConf(env=env, extras={
+ 'base': [
+ "LogLevel proxy:trace1 proxy_http:trace1 ssl:trace1 proxy_http2:trace1",
+ f"<Proxy https://127.0.0.1:{env.https_port}/>",
+ " SSLProxyEngine on",
+ " SSLProxyVerify require",
+ f" SSLProxyCACertificateFile {env.ca.cert_file}",
+ " ProxyPreserveHost on",
+ "</Proxy>",
+ f"<Proxy https://localhost:{env.https_port}/>",
+ " ProxyPreserveHost on",
+ "</Proxy>",
+ f"<Proxy h2://127.0.0.1:{env.https_port}/>",
+ " SSLProxyEngine on",
+ " SSLProxyVerify require",
+ f" SSLProxyCACertificateFile {env.ca.cert_file}",
+ " ProxyPreserveHost on",
+ "</Proxy>",
+ ],
+ env.domain_b: [
+ "Protocols h2 http/1.1",
+ f'ProxyPass /proxy-ssl/ https://127.0.0.1:{env.https_port}/',
+ f'ProxyPass /proxy-local/ https://localhost:{env.https_port}/',
+ f'ProxyPass /proxy-h2-ssl/ h2://127.0.0.1:{env.https_port}/',
+ "TLSOptions +StdEnvVars",
+ ],
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_tls_14_proxy_ssl_get(self, env):
+ data = env.tls_get_json(env.domain_b, "/proxy-ssl/index.json")
+ assert data == {'domain': env.domain_b}
+
+ def test_tls_14_proxy_ssl_get_local(self, env):
+ # does not work, since SSLProxy* not configured
+ data = env.tls_get_json(env.domain_b, "/proxy-local/index.json")
+ assert data is None
+
+ def test_tls_14_proxy_ssl_h2_get(self, env):
+ r = env.tls_get(env.domain_b, "/proxy-h2-ssl/index.json")
+ assert r.exit_code == 0
+ assert r.json == {'domain': env.domain_b}
+
+ @pytest.mark.parametrize("name, value", [
+ ("SERVER_NAME", "b.mod-tls.test"),
+ ("SSL_SESSION_RESUMED", "Initial"),
+ ("SSL_SECURE_RENEG", "false"),
+ ("SSL_COMPRESS_METHOD", "NULL"),
+ ("SSL_CIPHER_EXPORT", "false"),
+ ("SSL_CLIENT_VERIFY", "NONE"),
+ ])
+ def test_tls_14_proxy_ssl_vars_const(self, env, name: str, value: str):
+ r = env.tls_get(env.domain_b, f"/proxy-ssl/vars.py?name={name}")
+ assert r.exit_code == 0, r.stderr
+ assert r.json == {name: value}, r.stdout
+
+ @pytest.mark.parametrize("name, pattern", [
+ ("SSL_VERSION_INTERFACE", r'mod_tls/\d+\.\d+\.\d+'),
+ ("SSL_VERSION_LIBRARY", r'rustls-ffi/\d+\.\d+\.\d+/rustls/\d+\.\d+\.\d+'),
+ ])
+ def test_tls_14_proxy_ssl_vars_match(self, env, name: str, pattern: str):
+ r = env.tls_get(env.domain_b, f"/proxy-ssl/vars.py?name={name}")
+ assert r.exit_code == 0, r.stderr
+ assert name in r.json
+ assert re.match(pattern, r.json[name]), r.json
diff --git a/test/modules/tls/test_15_proxy_tls.py b/test/modules/tls/test_15_proxy_tls.py
new file mode 100644
index 0000000..f2f670d
--- /dev/null
+++ b/test/modules/tls/test_15_proxy_tls.py
@@ -0,0 +1,86 @@
+import re
+from datetime import timedelta
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestProxyTLS:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ # add vhosts a+b and a ssl proxy from a to b
+ conf = TlsTestConf(env=env, extras={
+ 'base': [
+ "LogLevel proxy:trace1 proxy_http:trace1 proxy_http2:trace2 http2:trace2 cgid:trace4",
+ "TLSProxyProtocol TLSv1.3+",
+ f"<Proxy https://127.0.0.1:{env.https_port}/>",
+ " TLSProxyEngine on",
+ f" TLSProxyCA {env.ca.cert_file}",
+ " TLSProxyProtocol TLSv1.2+",
+ " TLSProxyCiphersPrefer TLS13_AES_256_GCM_SHA384",
+ " TLSProxyCiphersSuppress TLS13_AES_128_GCM_SHA256",
+ " ProxyPreserveHost on",
+ "</Proxy>",
+ f"<Proxy https://localhost:{env.https_port}/>",
+ " ProxyPreserveHost on",
+ "</Proxy>",
+ f"<Proxy h2://127.0.0.1:{env.https_port}/>",
+ " TLSProxyEngine on",
+ f" TLSProxyCA {env.ca.cert_file}",
+ " TLSProxyCiphersSuppress TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256",
+ " ProxyPreserveHost on",
+ "</Proxy>",
+ ],
+ env.domain_b: [
+ "Protocols h2 http/1.1",
+ f"ProxyPass /proxy-tls/ https://127.0.0.1:{env.https_port}/",
+ f"ProxyPass /proxy-local/ https://localhost:{env.https_port}/",
+ f"ProxyPass /proxy-h2-tls/ h2://127.0.0.1:{env.https_port}/",
+ "TLSOptions +StdEnvVars",
+ ],
+ })
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_tls_15_proxy_tls_get(self, env):
+ data = env.tls_get_json(env.domain_b, "/proxy-tls/index.json")
+ assert data == {'domain': env.domain_b}
+
+ def test_tls_15_proxy_tls_get_local(self, env):
+ # does not work, since SSLProxy* not configured
+ data = env.tls_get_json(env.domain_b, "/proxy-local/index.json")
+ assert data is None
+
+ def test_tls_15_proxy_tls_h2_get(self, env):
+ r = env.tls_get(env.domain_b, "/proxy-h2-tls/index.json")
+ assert r.exit_code == 0
+ assert r.json == {'domain': env.domain_b}, f"{r.stdout}"
+
+ @pytest.mark.parametrize("name, value", [
+ ("SERVER_NAME", "b.mod-tls.test"),
+ ("SSL_PROTOCOL", "TLSv1.3"),
+ ("SSL_CIPHER", "TLS_AES_256_GCM_SHA384"),
+ ("SSL_SESSION_RESUMED", "Initial"),
+ ("SSL_SECURE_RENEG", "false"),
+ ("SSL_COMPRESS_METHOD", "NULL"),
+ ("SSL_CIPHER_EXPORT", "false"),
+ ("SSL_CLIENT_VERIFY", "NONE"),
+ ])
+ def test_tls_15_proxy_tls_h1_vars(self, env, name: str, value: str):
+ r = env.tls_get(env.domain_b, f"/proxy-tls/vars.py?name={name}")
+ assert r.exit_code == 0, r.stderr
+ assert r.json == {name: value}, r.stdout
+
+ @pytest.mark.parametrize("name, value", [
+ ("SERVER_NAME", "b.mod-tls.test"),
+ ("SSL_PROTOCOL", "TLSv1.3"),
+ ("SSL_CIPHER", "TLS_CHACHA20_POLY1305_SHA256"),
+ ("SSL_SESSION_RESUMED", "Initial"),
+ ])
+ def test_tls_15_proxy_tls_h2_vars(self, env, name: str, value: str):
+ r = env.tls_get(env.domain_b, f"/proxy-h2-tls/vars.py?name={name}")
+ assert r.exit_code == 0, r.stderr
+ assert r.json == {name: value}, r.stdout
diff --git a/test/modules/tls/test_16_proxy_mixed.py b/test/modules/tls/test_16_proxy_mixed.py
new file mode 100644
index 0000000..ca08236
--- /dev/null
+++ b/test/modules/tls/test_16_proxy_mixed.py
@@ -0,0 +1,47 @@
+import time
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestProxyMixed:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = TlsTestConf(env=env, extras={
+ 'base': [
+ "LogLevel proxy:trace1 proxy_http:trace1 ssl:trace1 proxy_http2:trace1 http2:debug",
+ "ProxyPreserveHost on",
+ ],
+ env.domain_a: [
+ "Protocols h2 http/1.1",
+ "TLSProxyEngine on",
+ f"TLSProxyCA {env.ca.cert_file}",
+ "<Location /proxy-tls/>",
+ f" ProxyPass h2://127.0.0.1:{env.https_port}/",
+ "</Location>",
+ ],
+ env.domain_b: [
+ "SSLProxyEngine on",
+ "SSLProxyVerify require",
+ f"SSLProxyCACertificateFile {env.ca.cert_file}",
+ "<Location /proxy-ssl/>",
+ f" ProxyPass https://127.0.0.1:{env.https_port}/",
+ "</Location>",
+ ],
+ })
+ # add vhosts a+b and a ssl proxy from a to b
+ conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_tls_16_proxy_mixed_ssl_get(self, env, repeat):
+ data = env.tls_get_json(env.domain_b, "/proxy-ssl/index.json")
+ assert data == {'domain': env.domain_b}
+
+ def test_tls_16_proxy_mixed_tls_get(self, env, repeat):
+ data = env.tls_get_json(env.domain_a, "/proxy-tls/index.json")
+ if data is None:
+ time.sleep(300)
+ assert data == {'domain': env.domain_a}
diff --git a/test/modules/tls/test_17_proxy_machine_cert.py b/test/modules/tls/test_17_proxy_machine_cert.py
new file mode 100644
index 0000000..7b5ef44
--- /dev/null
+++ b/test/modules/tls/test_17_proxy_machine_cert.py
@@ -0,0 +1,69 @@
+import os
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+class TestProxyMachineCert:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def clients_x(cls, env):
+ return env.ca.get_first("clientsX")
+
+ @pytest.fixture(autouse=True, scope='class')
+ def clients_y(cls, env):
+ return env.ca.get_first("clientsY")
+
+ @pytest.fixture(autouse=True, scope='class')
+ def cax_file(cls, clients_x):
+ return os.path.join(os.path.dirname(clients_x.cert_file), "clientsX-ca.pem")
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(cls, env, cax_file, clients_x):
+ # add vhosts a(tls)+b(ssl, port2) and a ssl proxy from a to b with a machine cert
+ # host b requires a client certificate
+ conf = TlsTestConf(env=env, extras={
+ 'base': [
+ "LogLevel proxy:trace1 proxy_http:trace1 ssl:trace4 proxy_http2:trace1",
+ "ProxyPreserveHost on",
+ f"Listen {env.proxy_port}",
+ ],
+ })
+ conf.start_tls_vhost(domains=[env.domain_a], port=env.https_port)
+ conf.add([
+ "Protocols h2 http/1.1",
+ "TLSProxyEngine on",
+ f"TLSProxyCA {env.ca.cert_file}",
+ f"TLSProxyMachineCertificate {clients_x.get_first('user1').cert_file}",
+ "<Location /proxy-tls/>",
+ f" ProxyPass https://127.0.0.1:{env.proxy_port}/",
+ "</Location>",
+ ])
+ conf.end_tls_vhost()
+ conf.start_vhost(domains=[env.domain_a], port=env.proxy_port,
+ doc_root=f"htdocs/{env.domain_a}", with_ssl=True)
+ conf.add([
+ "SSLVerifyClient require",
+ "SSLVerifyDepth 2",
+ "SSLOptions +StdEnvVars +ExportCertData",
+ f"SSLCACertificateFile {cax_file}",
+ "SSLUserName SSL_CLIENT_S_DN_CN"
+ ])
+ conf.end_vhost()
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_tls_17_proxy_machine_cert_get_a(self, env):
+ data = env.tls_get_json(env.domain_a, "/proxy-tls/index.json")
+ assert data == {'domain': env.domain_a}
+
+ @pytest.mark.parametrize("name, value", [
+ ("SERVER_NAME", "a.mod-tls.test"),
+ ("SSL_CLIENT_VERIFY", "SUCCESS"),
+ ("REMOTE_USER", "user1"),
+ ])
+ def test_tls_17_proxy_machine_cert_vars(self, env, name: str, value: str):
+ r = env.tls_get(env.domain_a, f"/proxy-tls/vars.py?name={name}")
+ assert r.exit_code == 0, r.stderr
+ assert r.json == {name: value}, r.stdout