]> git.ipfire.org Git - thirdparty/apache/httpd.git/commitdiff
*) test: adding modules/tls test cases from trunk.
authorStefan Eissing <icing@apache.org>
Tue, 14 Dec 2021 11:40:07 +0000 (11:40 +0000)
committerStefan Eissing <icing@apache.org>
Tue, 14 Dec 2021 11:40:07 +0000 (11:40 +0000)
git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/branches/2.4.x@1895948 13f79535-47bb-0310-9956-ffa450edef68

29 files changed:
test/modules/tls/__init__.py [new file with mode: 0644]
test/modules/tls/conf.py [new file with mode: 0644]
test/modules/tls/conftest.py [new file with mode: 0644]
test/modules/tls/env.py [new file with mode: 0644]
test/modules/tls/htdocs/a.mod-tls.test/index.json [new file with mode: 0644]
test/modules/tls/htdocs/a.mod-tls.test/vars.py [new file with mode: 0755]
test/modules/tls/htdocs/b.mod-tls.test/dir1/vars.py [new file with mode: 0755]
test/modules/tls/htdocs/b.mod-tls.test/index.json [new file with mode: 0644]
test/modules/tls/htdocs/b.mod-tls.test/resp-jitter.py [new file with mode: 0755]
test/modules/tls/htdocs/b.mod-tls.test/vars.py [new file with mode: 0755]
test/modules/tls/htdocs/index.html [new file with mode: 0644]
test/modules/tls/htdocs/index.json [new file with mode: 0644]
test/modules/tls/test_01_apache.py [new file with mode: 0644]
test/modules/tls/test_02_conf.py [new file with mode: 0644]
test/modules/tls/test_03_sni.py [new file with mode: 0644]
test/modules/tls/test_04_get.py [new file with mode: 0644]
test/modules/tls/test_05_proto.py [new file with mode: 0644]
test/modules/tls/test_06_ciphers.py [new file with mode: 0644]
test/modules/tls/test_07_alpn.py [new file with mode: 0644]
test/modules/tls/test_08_vars.py [new file with mode: 0644]
test/modules/tls/test_09_timeout.py [new file with mode: 0644]
test/modules/tls/test_10_session_id.py [new file with mode: 0644]
test/modules/tls/test_11_md.py [new file with mode: 0644]
test/modules/tls/test_12_cauth.py [new file with mode: 0644]
test/modules/tls/test_13_proxy.py [new file with mode: 0644]
test/modules/tls/test_14_proxy_ssl.py [new file with mode: 0644]
test/modules/tls/test_15_proxy_tls.py [new file with mode: 0644]
test/modules/tls/test_16_proxy_mixed.py [new file with mode: 0644]
test/modules/tls/test_17_proxy_machine_cert.py [new file with mode: 0644]

diff --git a/test/modules/tls/__init__.py b/test/modules/tls/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/test/modules/tls/conf.py b/test/modules/tls/conf.py
new file mode 100644 (file)
index 0000000..ddeb91f
--- /dev/null
@@ -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 (file)
index 0000000..cde4be6
--- /dev/null
@@ -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 (file)
index 0000000..e60c321
--- /dev/null
@@ -0,0 +1,186 @@
+import inspect
+import logging
+import os
+import re
+import subprocess
+import sys
+import time
+
+from datetime import timedelta, datetime
+from http.client import HTTPConnection
+from typing import List, Optional, Dict, Tuple, Union
+from urllib.parse import urlparse
+
+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):
+
+    # 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)
+
+    CURL_SUPPORTS_TLS_1_3 = None
+
+    def curl_supports_tls_1_3(self) -> bool:
+        if self.CURL_SUPPORTS_TLS_1_3 is None:
+            r = self.tls_get(self.domain_a, "/index.json", options=["--tlsv1.3"])
+            self.CURL_SUPPORTS_TLS_1_3 = r.exit_code == 0
+        return self.CURL_SUPPORTS_TLS_1_3
+
+    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 (file)
index 0000000..ffc32cb
--- /dev/null
@@ -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 (executable)
index 0000000..4ab491b
--- /dev/null
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+import json
+import os, cgi
+import re
+
+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 = None
+try:
+    form = cgi.FieldStorage()
+    if 'name' in form:
+        name = str(form['name'].value)
+except Exception:
+    pass
+
+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 (executable)
index 0000000..b86a968
--- /dev/null
@@ -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 (file)
index 0000000..e5d3ccf
--- /dev/null
@@ -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 (executable)
index 0000000..f7b1349
--- /dev/null
@@ -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 (executable)
index 0000000..9289c5c
--- /dev/null
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+import json
+import os, cgi
+
+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 = None
+try:
+    form = cgi.FieldStorage()
+    if 'name' in form:
+        name = str(form['name'].value)
+except Exception:
+    pass
+
+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 (file)
index 0000000..3c07626
--- /dev/null
@@ -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 (file)
index 0000000..6d456e0
--- /dev/null
@@ -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 (file)
index 0000000..eb9b32e
--- /dev/null
@@ -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_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 (file)
index 0000000..f34b85e
--- /dev/null
@@ -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_02_conf_cert_args_missing(self, env):
+        conf = TlsTestConf(env=env)
+        conf.add("TLSCertificate")
+        conf.install()
+        assert env.apache_fail() == 0
+
+    def test_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_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_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_02_conf_cert_listen_missing(self, env):
+        conf = TlsTestConf(env=env)
+        conf.add("TLSEngine")
+        conf.install()
+        assert env.apache_fail() == 0
+
+    def test_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_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_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_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_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_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_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_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_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 (file)
index 0000000..2873b65
--- /dev/null
@@ -0,0 +1,73 @@
+from datetime import timedelta
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+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
+        env.curl_supports_tls_1_3()  # init
+
+    @pytest.fixture(autouse=True, scope='function')
+    def _function_scope(self, env):
+        pass
+
+    def test_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_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_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_03_sni_request_other_same_config(self, env):
+        # do we see the first vhost respone 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_03_sni_request_other_other_honor(self, env):
+        if env.curl_supports_tls_1_3():
+            # can't do this test then
+            return
+        # do we see the first vhost respone 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", "--header", "Host: {0}".format(env.domain_b)
+        ])
+        # request denied
+        assert r.exit_code == 0
+        assert r.json is None
+
+    def test_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 (file)
index 0000000..6621f8a
--- /dev/null
@@ -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_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_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 (file)
index 0000000..7165f21
--- /dev/null
@@ -0,0 +1,67 @@
+import time
+from datetime import timedelta
+import socket
+from threading import Thread
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+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
+
+    CURL_SUPPORTS_TLS_1_3 = None
+
+    def test_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 env.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_05_proto_1_3(self, env):
+        r = env.tls_get(env.domain_a, "/index.json", options=["--tlsv1.3"])
+        if env.curl_supports_tls_1_3():
+            assert r.exit_code == 0, r.stderr
+        else:
+            assert r.exit_code == 4, r.stderr
+
+    def test_05_proto_close(self, env):
+        s = socket.create_connection(('localhost', env.https_port))
+        time.sleep(0.1)
+        s.close()
+
+    def test_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 (file)
index 0000000..60a5e20
--- /dev/null
@@ -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_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_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_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_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_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_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_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_06_ciphers_pref_unsupported(self, env):
+        # a warning on prefering 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_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_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 (file)
index 0000000..9f70f5f
--- /dev/null
@@ -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(1)
+        return None
+
+    def test_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"])
+        assert r.exit_code == 0, r.stderr
+        protocol = self._get_protocol(r.stderr)
+        assert protocol == "http/1.1", r.stderr
+
+    def test_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 (file)
index 0000000..baaed38
--- /dev/null
@@ -0,0 +1,63 @@
+import re
+from datetime import timedelta
+
+import pytest
+
+from .conf import TlsTestConf
+
+
+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_08_vars_root(self, env):
+        # in domain_b root, the StdEnvVars is switch on
+        if env.curl_supports_tls_1_3():
+            exp_proto = "TLSv1.3"
+            exp_cipher = "TLS_AES_256_GCM_SHA384"
+        else:
+            exp_proto = "TLSv1.2"
+            exp_cipher = "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
+        r = env.tls_get(env.domain_b, "/vars.py")
+        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,
+        }, r.stdout
+
+    @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_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_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 (file)
index 0000000..567f256
--- /dev/null
@@ -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_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 (file)
index 0000000..1ac03e9
--- /dev/null
@@ -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_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_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 (file)
index 0000000..de318c9
--- /dev/null
@@ -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_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_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_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 (file)
index 0000000..580df77
--- /dev/null
@@ -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_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_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_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_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_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_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_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_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_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 (file)
index 0000000..affea97
--- /dev/null
@@ -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_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_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 (file)
index 0000000..18b7b0f
--- /dev/null
@@ -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_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_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_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_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_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 (file)
index 0000000..efc2701
--- /dev/null
@@ -0,0 +1,89 @@
+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_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_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_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_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"),
+        ("SERVER_NAME", "b.mod-tls.test"),
+        ("SERVER_NAME", "b.mod-tls.test"),
+        ("SERVER_NAME", "b.mod-tls.test"),
+        ("SSL_PROTOCOL", "TLSv1.3"),
+        ("SSL_CIPHER", "TLS_CHACHA20_POLY1305_SHA256"),
+        ("SSL_SESSION_RESUMED", "Initial"),
+    ])
+    def test_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 (file)
index 0000000..a37c530
--- /dev/null
@@ -0,0 +1,43 @@
+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",
+                "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_16_proxy_mixed_ssl_get(self, env):
+        data = env.tls_get_json(env.domain_b, "/proxy-ssl/index.json")
+        assert data == {'domain': env.domain_b}
+
+    def test_16_proxy_mixed_tls_get(self, env):
+        data = env.tls_get_json(env.domain_a, "/proxy-tls/index.json")
+        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 (file)
index 0000000..efaa15a
--- /dev/null
@@ -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_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_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