diff options
Diffstat (limited to '')
29 files changed, 1803 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..a39fcaa --- /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) -> 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) + + 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..f41ec6a --- /dev/null +++ b/test/modules/tls/htdocs/a.mod-tls.test/vars.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +import json +import os, sys +import multipart +from urllib import parse + + +def get_request_params(): + oforms = {} + 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] + myenv = os.environ.copy() + myenv['wsgi.input'] = sys.stdin.buffer + mforms, ofiles = multipart.parse_form_data(environ=myenv) + for name, item in mforms.items(): + oforms[name] = item + 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..f41ec6a --- /dev/null +++ b/test/modules/tls/htdocs/b.mod-tls.test/vars.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +import json +import os, sys +import multipart +from urllib import parse + + +def get_request_params(): + oforms = {} + 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] + myenv = os.environ.copy() + myenv['wsgi.input'] = sys.stdin.buffer + mforms, ofiles = multipart.parse_form_data(environ=myenv) + for name, item in mforms.items(): + oforms[name] = item + 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..4412a66 --- /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, 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..447d052 --- /dev/null +++ b/test/modules/tls/test_05_proto.py @@ -0,0 +1,66 @@ +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 + if TlsTestEnv.curl_supports_tls_1_3(): + r = env.tls_get(env.domain_b, "/index.json", options=["--tlsv1.3"]) + assert r.exit_code == 0, r.stderr + + def test_tls_05_proto_1_3(self, env): + r = env.tls_get(env.domain_a, "/index.json", options=["--tlsv1.3"]) + if TlsTestEnv.curl_supports_tls_1_3(): + assert r.exit_code == 0, r.stderr + else: + assert r.exit_code == 4, r.stderr + + 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 |