You are viewing a plain text version of this content. The canonical link for it is here.
Posted to cvs@httpd.apache.org by ic...@apache.org on 2021/10/29 10:05:30 UTC

svn commit: r1894611 [4/5] - in /httpd/httpd/trunk: ./ test/ test/modules/md/ test/modules/md/data/ test/modules/md/data/store_migrate/ test/modules/md/data/store_migrate/1.0/ test/modules/md/data/store_migrate/1.0/sample1/ test/modules/md/data/store_m...

Added: httpd/httpd/trunk/test/modules/md/test_702_auto.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_702_auto.py?rev=1894611&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_702_auto.py (added)
+++ httpd/httpd/trunk/test/modules/md/test_702_auto.py Fri Oct 29 10:05:29 2021
@@ -0,0 +1,750 @@
+import os
+import pytest
+
+from pyhttpd.conf import HttpdConf
+from pyhttpd.env import HttpdTestEnv
+from .md_cert_util import MDCertUtil
+from .md_env import MDTestEnv
+from .md_conf import MDConf
+
+
+@pytest.mark.skipif(condition=not MDTestEnv.has_acme_server(),
+                    reason="no ACME test server configured")
+class TestAutov2:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env, acme):
+        env.APACHE_CONF_SRC = "data/test_auto"
+        acme.start(config='default')
+        env.check_acme()
+        env.clear_store()
+        MDConf(env).install()
+        assert env.apache_restart() == 0
+
+    @pytest.fixture(autouse=True, scope='function')
+    def _method_scope(self, env, request):
+        env.clear_store()
+        self.test_domain = env.get_request_domain(request)
+
+    def _write_res_file(self, doc_root, name, content):
+        if not os.path.exists(doc_root):
+            os.makedirs(doc_root)
+        open(os.path.join(doc_root, name), "w").write(content)
+
+    # create a MD not used in any virtual host, auto drive should NOT pick it up
+    def test_md_702_001(self, env):
+        domain = self.test_domain
+        # generate config with one MD
+        domains = [domain, "www." + domain]
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_drive_mode("auto")
+        conf.add_md(domains)
+        conf.install()
+        #
+        # restart, check that MD is synched to store
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        stat = env.get_md_status(domain)
+        assert stat["watched"] == 0
+        #
+        # add vhost for MD, restart should drive it
+        conf.add_vhost(domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion([domain])
+        env.check_md_complete(domain)
+        stat = env.get_md_status(domain)
+        assert stat["watched"] == 1
+        cert = env.get_cert(domain)
+        assert domain in cert.get_san_list()
+        #
+        # challenges should have been removed
+        # file system needs to have correct permissions
+        env.check_dir_empty(env.store_challenges())
+        env.check_file_permissions(domain)
+
+    # test case: same as test_702_001, but with two parallel managed domains
+    def test_md_702_002(self, env):
+        domain = self.test_domain
+        domain_a = "a-" + domain
+        domain_b = "b-" + domain
+        #        
+        # generate config with two MDs
+        domains_a = [domain_a, "www." + domain_a]
+        domains_b = [domain_b, "www." + domain_b]
+        conf = MDConf(env)
+        conf.add_drive_mode("auto")
+        conf.add_md(domains_a)
+        conf.add_md(domains_b)
+        conf.add_vhost(domains_a)
+        conf.add_vhost(domains_b)
+        conf.install()
+        #
+        # restart, check that md is in store
+        assert env.apache_restart() == 0
+        env.check_md(domains_a)
+        env.check_md(domains_b)
+        #
+        # await drive completion, do not restart
+        assert env.await_completion([domain_a, domain_b], restart=False)
+        # staged certificates are now visible on the status resources
+        status = env.get_md_status(domain_a)
+        assert 'renewal' in status
+        assert 'cert' in status['renewal']
+        assert 'rsa' in status['renewal']['cert']
+        assert 'sha256-fingerprint' in status['renewal']['cert']['rsa']
+        # check the non-staged status
+        assert status['state'] == 1
+        assert status['state-descr'] == "certificate(rsa) is missing"
+
+        # restart and activate
+        assert env.apache_restart() == 0
+        # check: SSL is running OK
+        cert_a = env.get_cert(domain_a)
+        assert domains_a == cert_a.get_san_list()
+        cert_b = env.get_cert(domain_b)
+        assert domains_b == cert_b.get_san_list()
+        # check that we created only one account
+        md_a = env.get_md_status(domain_a)
+        md_b = env.get_md_status(domain_b)
+        assert md_a['ca'] == md_b['ca']
+
+    # test case: one MD, that covers two vhosts
+    def test_md_702_003(self, env):
+        domain = self.test_domain
+        name_a = "test-a." + domain
+        name_b = "test-b." + domain
+        domains = [domain, name_a, name_b]
+        #
+        # generate 1 MD and 2 vhosts
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_md(domains)
+        conf.add_vhost(name_a, doc_root="htdocs/a")
+        conf.add_vhost(name_b, doc_root="htdocs/b")
+        conf.install()
+        #
+        # create docRoot folder
+        self._write_res_file(os.path.join(env.server_docs_dir, "a"), "name.txt", name_a)
+        self._write_res_file(os.path.join(env.server_docs_dir, "b"), "name.txt", name_b)
+        #
+        # restart (-> drive), check that MD was synched and completes
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        assert env.await_completion([domain])
+        env.check_md_complete(domain)
+        #
+        # check: SSL is running OK
+        cert_a = env.get_cert(name_a)
+        assert name_a in cert_a.get_san_list()
+        cert_b = env.get_cert(name_b)
+        assert name_b in cert_b.get_san_list()
+        assert cert_a.same_serial_as(cert_b)
+        #
+        assert env.get_content(name_a, "/name.txt") == name_a
+        assert env.get_content(name_b, "/name.txt") == name_b
+
+    # test case: drive with using single challenge type explicitly
+    @pytest.mark.parametrize("challenge_type", [
+        "tls-alpn-01", "http-01",
+    ])
+    def test_md_702_004(self, env, challenge_type):
+        domain = self.test_domain
+        domains = [domain, "www." + domain]
+        #
+        # generate 1 MD and 1 vhost
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add("Protocols http/1.1 acme-tls/1")
+        conf.add_drive_mode("auto")
+        conf.add(f"MDCAChallenges {challenge_type}")
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+        #
+        # restart (-> drive), check that MD was synched and completes
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        assert env.await_completion([domain])
+        env.check_md_complete(domain)
+        #        
+        # check SSL running OK
+        cert = env.get_cert(domain)
+        assert domain in cert.get_san_list()
+
+    # test case: drive_mode manual, check that server starts, but requests to domain are 503'd
+    def test_md_702_005(self, env):
+        domain = self.test_domain
+        name_a = "test-a." + domain
+        domains = [domain, name_a]
+        #
+        # generate 1 MD and 1 vhost
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_drive_mode("manual")
+        conf.add_md(domains)
+        conf.add_vhost(name_a, doc_root="htdocs/a")
+        conf.install()
+        #
+        # create docRoot folder
+        self._write_res_file(os.path.join(env.server_docs_dir, "a"), "name.txt", name_a)
+        #
+        # restart, check that md is in store
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        #        
+        # check: that request to domains give 503 Service Unavailable
+        cert1 = env.get_cert(name_a)
+        assert name_a in cert1.get_san_list()
+        assert env.get_http_status(name_a, "/name.txt") == 503
+        #
+        # check temporary cert from server
+        cert2 = MDCertUtil(env.path_fallback_cert(domain))
+        assert cert1.same_serial_as(cert2), \
+            "Unexpected temporary certificate on vhost %s. Expected cn: %s , "\
+            "but found cn: %s" % (name_a, cert2.get_cn(), cert1.get_cn())
+
+    # test case: drive MD with only invalid challenges, domains should stay 503'd
+    def test_md_702_006(self, env):
+        domain = self.test_domain
+        name_a = "test-a." + domain
+        domains = [domain, name_a]
+        #
+        # generate 1 MD, 1 vhost
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add("MDCAChallenges invalid-01 invalid-02")
+        conf.add_md(domains)
+        conf.add_vhost(name_a, doc_root="htdocs/a")
+        conf.install()
+        #
+        # create docRoot folder
+        self._write_res_file(os.path.join(env.server_docs_dir, "a"), "name.txt", name_a)
+        #
+        # restart, check that md is in store
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        # await drive completion
+        md = env.await_error(domain)
+        assert md
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'challenge-mismatch'
+        assert 'account' not in md['ca']
+        #
+        # check: that request to domains give 503 Service Unavailable
+        cert = env.get_cert(name_a)
+        assert name_a in cert.get_san_list()
+        assert env.get_http_status(name_a, "/name.txt") == 503
+
+    # Specify a non-working http proxy
+    def test_md_702_008(self, env):
+        domain = self.test_domain
+        domains = [domain]
+        #
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_drive_mode("always")
+        conf.add("MDHttpProxy http://localhost:1")
+        conf.add_md(domains)
+        conf.install()
+        #
+        # - restart (-> drive)
+        assert env.apache_restart() == 0
+        # await drive completion
+        md = env.await_error(domain)
+        assert md
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['status-description'] == 'Connection refused'
+        assert 'account' not in md['ca']
+
+    # Specify a valid http proxy
+    def test_md_702_008a(self, env):
+        domain = self.test_domain
+        domains = [domain]
+        #
+        conf = MDConf(env, admin=f"admin@{domain}", proxy=True)
+        conf.add_drive_mode("always")
+        conf.add(f"MDHttpProxy http://localhost:{env.proxy_port}")
+        conf.add_md(domains)
+        conf.install()
+        #
+        # - restart (-> drive), check that md is in store
+        assert env.apache_restart() == 0
+        assert env.await_completion([domain])
+        assert env.apache_restart() == 0
+        env.check_md_complete(domain)
+
+    # Force cert renewal due to critical remaining valid duration
+    # Assert that new cert activation is delayed
+    def test_md_702_009(self, env):
+        domain = self.test_domain
+        domains = [domain]
+        #
+        # prepare md
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_drive_mode("auto")
+        conf.add_renew_window("10d")
+        conf.add_md(domains)
+        conf.add_vhost(domain)
+        conf.install()
+        #
+        # restart (-> drive), check that md+cert is in store, TLS is up
+        assert env.apache_restart() == 0
+        assert env.await_completion([domain])
+        env.check_md_complete(domain)
+        cert1 = MDCertUtil(env.store_domain_file(domain, 'pubcert.pem'))
+        # compare with what md reports as status
+        stat = env.get_certificate_status(domain)
+        assert cert1.same_serial_as(stat['rsa']['serial'])
+        #
+        # create self-signed cert, with critical remaining valid duration -> drive again
+        env.create_self_signed_cert([domain], {"notBefore": -120, "notAfter": 2}, serial=7029)
+        cert3 = MDCertUtil(env.store_domain_file(domain, 'pubcert.pem'))
+        assert cert3.same_serial_as('1B75')
+        assert env.apache_restart() == 0
+        stat = env.get_certificate_status(domain)
+        assert cert3.same_serial_as(stat['rsa']['serial'])
+        #
+        # cert should renew and be different afterwards
+        assert env.await_completion([domain], must_renew=True)
+        stat = env.get_certificate_status(domain)
+        assert not cert3.same_serial_as(stat['rsa']['serial'])
+        
+    # test case: drive with an unsupported challenge due to port availability 
+    def test_md_702_010(self, env):
+        domain = self.test_domain
+        domains = [domain, "www." + domain]
+        #
+        # generate 1 MD and 1 vhost, map port 80 to where the server does not listen
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_drive_mode("auto")
+        conf.add("MDPortMap 80:99")        
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        md = env.await_error(domain)
+        assert md["renewal"]["errors"] > 0
+        #
+        # now the same with a 80 mapped to a supported port 
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_drive_mode("auto")
+        conf.add("MDCAChallenges http-01")
+        conf.add("MDPortMap 80:%s" % env.http_port)
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        assert env.await_completion([domain])
+
+    def test_md_702_011(self, env):
+        domain = self.test_domain
+        domains = [domain, "www." + domain]
+        #
+        # generate 1 MD and 1 vhost, map port 443 to where the server does not listen
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add("Protocols http/1.1 acme-tls/1")
+        conf.add_drive_mode("auto")
+        conf.add("MDPortMap https:99 http:99")        
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        md = env.await_error(domain)
+        assert md["renewal"]["errors"] > 0
+        #
+        # now the same with a 443 mapped to a supported port 
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add("Protocols http/1.1 acme-tls/1")
+        conf.add_drive_mode("auto")
+        conf.add("MDCAChallenges tls-alpn-01")
+        conf.add("MDPortMap https:%s" % env.https_port)
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        assert env.await_completion([domain])
+
+    # test case: one MD with several dns names. sign up. remove the *first* name
+    # in the MD. restart. should find and keep the existing MD.
+    # See: https://github.com/icing/mod_md/issues/68
+    def test_md_702_030(self, env):
+        domain = self.test_domain
+        name_x = "test-x." + domain
+        name_a = "test-a." + domain
+        name_b = "test-b." + domain
+        domains = [name_x, name_a, name_b]
+        #
+        # generate 1 MD and 2 vhosts
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_md(domains)
+        conf.add_vhost(name_a)
+        conf.add_vhost(name_b)
+        conf.install()
+        #
+        # restart (-> drive), check that MD was synched and completes
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        assert env.await_completion([name_x])
+        env.check_md_complete(name_x)
+        #
+        # check: SSL is running OK
+        cert_a = env.get_cert(name_a)
+        assert name_a in cert_a.get_san_list()
+        cert_b = env.get_cert(name_b)
+        assert name_b in cert_b.get_san_list()
+        assert cert_a.same_serial_as(cert_b)
+        #        
+        # change MD by removing 1st name
+        new_list = [name_a, name_b]
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_md(new_list)
+        conf.add_vhost(name_a)
+        conf.add_vhost(name_b)
+        conf.install()
+        # restart, check that host still works and kept the cert
+        assert env.apache_restart() == 0
+        env.check_md(new_list)
+        status = env.get_certificate_status(name_a)
+        assert cert_a.same_serial_as(status['rsa']['serial'])
+
+    # test case: Same as 7030, but remove *and* add another at the same time.
+    # restart. should find and keep the existing MD and renew for additional name.
+    # See: https://github.com/icing/mod_md/issues/68
+    def test_md_702_031(self, env):
+        domain = self.test_domain
+        name_x = "test-x." + domain
+        name_a = "test-a." + domain
+        name_b = "test-b." + domain
+        name_c = "test-c." + domain
+        domains = [name_x, name_a, name_b]
+        #
+        # generate 1 MD and 2 vhosts
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_md(domains)
+        conf.add_vhost(name_a)
+        conf.add_vhost(name_b)
+        conf.install()
+        #
+        # restart (-> drive), check that MD was synched and completes
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        assert env.await_completion([name_x])
+        env.check_md_complete(name_x)
+        #
+        # check: SSL is running OK
+        cert_a = env.get_cert(name_a)
+        assert name_a in cert_a.get_san_list()
+        cert_b = env.get_cert(name_b)
+        assert name_b in cert_b.get_san_list()
+        assert cert_a.same_serial_as(cert_b)
+        #        
+        # change MD by removing 1st name and adding another
+        new_list = [name_a, name_b, name_c]
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_md(new_list)
+        conf.add_vhost(name_a)
+        conf.add_vhost(name_b)
+        conf.install()
+        # restart, check that host still works and have new cert
+        assert env.apache_restart() == 0
+        env.check_md(new_list)
+        assert env.await_completion([name_a])
+        #
+        cert_a2 = env.get_cert(name_a)
+        assert name_a in cert_a2.get_san_list()
+        assert not cert_a.same_serial_as(cert_a2)
+
+    # test case: create two MDs, move them into one
+    # see: <https://bz.apache.org/bugzilla/show_bug.cgi?id=62572>
+    def test_md_702_032(self, env):
+        domain = self.test_domain
+        name1 = "server1." + domain
+        name2 = "server2.b" + domain  # need a separate TLD to avoid rate limites
+        #
+        # generate 2 MDs and 2 vhosts
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add("MDMembers auto")
+        conf.add_md([name1])
+        conf.add_md([name2])
+        conf.add_vhost(name1)
+        conf.add_vhost(name2)
+        conf.install()
+        #
+        # restart (-> drive), check that MD was synched and completes
+        assert env.apache_restart() == 0
+        env.check_md([name1])
+        env.check_md([name2])
+        assert env.await_completion([name1, name2])
+        env.check_md_complete(name2)
+        #
+        # check: SSL is running OK
+        cert1 = env.get_cert(name1)
+        assert name1 in cert1.get_san_list()
+        cert2 = env.get_cert(name2)
+        assert name2 in cert2.get_san_list()
+        #        
+        # remove second md and vhost, add name2 to vhost1
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add("MDMembers auto")
+        conf.add_md([name1])
+        conf.add_vhost([name1, name2])
+        conf.install()
+        assert env.apache_restart() == 0
+        env.check_md([name1, name2])
+        assert env.await_completion([name1])
+        #
+        cert1b = env.get_cert(name1)
+        assert name1 in cert1b.get_san_list()
+        assert name2 in cert1b.get_san_list()
+        assert not cert1.same_serial_as(cert1b)
+
+    # test case: test "tls-alpn-01" challenge handling
+    def test_md_702_040(self, env):
+        domain = self.test_domain
+        domains = [domain, "www." + domain]
+        #
+        # generate 1 MD and 1 vhost
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add("LogLevel core:debug")
+        conf.add("Protocols http/1.1 acme-tls/1")
+        conf.add_drive_mode("auto")
+        conf.add("MDCAChallenges tls-alpn-01")
+        conf.add_md(domains)
+        conf.add_vhost(domains=domains)
+        conf.install()
+        #
+        # restart (-> drive), check that MD was synched and completes
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        # check that acme-tls/1 is available for all domains
+        stat = env.get_md_status(domain)
+        assert stat["proto"]["acme-tls/1"] == domains
+        assert env.await_completion([domain])
+        env.check_md_complete(domain)
+        #        
+        # check SSL running OK
+        cert = env.get_cert(domain)
+        assert domain in cert.get_san_list()
+
+    # test case: test "tls-alpn-01" without enabling 'acme-tls/1' challenge protocol
+    def test_md_702_041(self, env):
+        domain = self.test_domain
+        domains = [domain, "www." + domain]
+        #
+        # generate 1 MD and 1 vhost
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add("LogLevel core:debug")
+        conf.add_drive_mode("auto")
+        conf.add("MDCAChallenges tls-alpn-01")
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+        #
+        # restart (-> drive), check that MD job shows errors 
+        # and that missing proto is detected
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        # check that acme-tls/1 is available for none of the domains
+        stat = env.get_md_status(domain)
+        assert stat["proto"]["acme-tls/1"] == []
+
+    # test case: 2.4.40 mod_ssl stumbles over a SSLCertificateChainFile when installing
+    # a fallback certificate
+    @pytest.mark.skipif(HttpdTestEnv.get_ssl_module() != "ssl", reason="only for mod_ssl")
+    def test_md_702_042(self, env):
+        domain = self.test_domain
+        dns_list = [domain]
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add("LogLevel core:debug")
+        cred = env.get_credentials_for_name(f"test1.{env.http_tld}")[0]
+        conf.add(f"SSLCertificateChainFile {cred.cert_file}")
+        conf.add_drive_mode("auto")
+        conf.add_md(dns_list)
+        conf.add_vhost(dns_list)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion([domain])
+
+    # test case: test "tls-alpn-01" without enabling 'acme-tls/1' challenge protocol
+    # and fallback "http-01" configured, see https://github.com/icing/mod_md/issues/255
+    def test_md_702_043(self, env):
+        domain = self.test_domain
+        domains = [domain, "www." + domain]
+        #
+        # generate 1 MD and 1 vhost
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add("LogLevel core:debug")
+        conf.add_drive_mode("auto")
+        conf.add("MDPortMap 80:%s" % env.http_port)
+        conf.add("MDCAChallenges tls-alpn-01 http-01")
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+        #
+        # restart (-> drive), check that MD job shows errors
+        # and that missing proto is detected
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        # check that acme-tls/1 is available for none of the domains
+        stat = env.get_md_status(domain)
+        assert stat["proto"]["acme-tls/1"] == []
+        # but make sure it completes nevertheless
+        assert env.await_completion([domain])
+
+    # test case: drive with using single challenge type explicitly
+    # and make sure that dns names not mapped to a VirtualHost also work
+    @pytest.mark.parametrize("challenge_type", [
+        "tls-alpn-01"  # , "http-01",
+    ])
+    def test_md_702_044(self, env, challenge_type):
+        domain = self.test_domain
+        md_domains = [domain, "mail." + domain]
+        domains = [domain]
+        #
+        # generate 1 MD and 1 vhost
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add("Protocols http/1.1 acme-tls/1")
+        conf.add_drive_mode("auto")
+        conf.add(f"MDCAChallenges {challenge_type}")
+        conf.add_md(md_domains)
+        conf.add_vhost(domains)
+        conf.install()
+        #
+        # restart (-> drive), check that MD was synched and completes
+        assert env.apache_restart() == 0
+        env.check_md(md_domains)
+        assert env.await_completion([domain])
+        env.check_md_complete(domain)
+        #
+        # check SSL running OK
+        cert = env.get_cert(domain)
+        assert md_domains[0] in cert.get_san_list()
+        assert md_domains[1] in cert.get_san_list()
+
+    # Make a setup using the base server. It will use http-01 challenge.
+    def test_md_702_050(self, env):
+        domain = self.test_domain
+        conf = MDConf(env, admin=f"admin@{domain}")
+        conf.add(f"""
+            MDBaseServer on
+            ServerName {domain}
+            """)
+        conf.add_md([domain])
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion([domain])
+
+    # Make a setup using the base server without http:, will fail.
+    def test_md_702_051(self, env):
+        domain = self.test_domain
+        conf = MDConf(env, admin=f"admin@{domain}")
+        conf.add(f"""
+            MDBaseServer on
+            MDPortMap http:-
+            ServerName {domain}
+            """)
+        conf.add_md([domain])
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_error(domain)
+
+    # Make a setup using the base server without http:, but with acme-tls/1, should work.
+    def test_md_702_052(self, env):
+        domain = self.test_domain
+        conf = MDConf(env, std_vhosts=False, admin=f"admin@{domain}")
+        conf.add([
+            "MDBaseServer on",
+            "MDPortMap http:-",
+            "Protocols h2 http/1.1 acme-tls/1",
+            f"ServerName {domain}",
+            "<IfModule ssl_module>",
+            "  SSLEngine on",
+            "</IfModule>",
+            ])
+        conf.add_md([domain])
+        conf.install()
+        assert env.apache_restart() == 0
+        stat = env.get_md_status(domain, via_domain=env.http_addr, use_https=False)
+        assert stat["proto"]["acme-tls/1"] == [domain]
+        assert env.await_completion([domain], via_domain=env.http_addr, use_https=False)
+
+    # Test a domain name longer than 64 chars, but components < 64, see #227
+    # Background: DNS has an official limit of 253 ASCII chars and components must be
+    # of length [1, 63].
+    # However the CN in a certificate is restricted too, see
+    # <https://github.com/letsencrypt/boulder/issues/2093>.
+    @pytest.mark.skipif(MDTestEnv.is_pebble(), reason="pebble differs here from boulder")
+    @pytest.mark.parametrize("challenge_type", [
+        "tls-alpn-01", "http-01"
+    ])
+    def test_md_702_060(self, env, challenge_type):
+        domain = self.test_domain
+        # use only too long names, this is expected to fail:
+        # see <https://github.com/jetstack/cert-manager/issues/1462>
+        long_domain = ("x" * (65 - len(domain))) + domain
+        domains = [long_domain, "www." + long_domain]
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add("Protocols http/1.1 acme-tls/1")
+        conf.add_drive_mode("auto")
+        conf.add(f"MDCAChallenges {challenge_type}")
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        assert env.await_error(long_domain)
+        # add a short domain to the SAN list, the CA should now use that one
+        # and issue a cert.
+        long_domain = ("y" * (65 - len(domain))) + domain
+        domains = [long_domain, "www." + long_domain, "xxx." + domain]
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add("Protocols http/1.1 acme-tls/1")
+        conf.add_drive_mode("auto")
+        conf.add(f"MDCAChallenges {challenge_type}")
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion([long_domain])
+        env.check_md_complete(long_domain)
+        #
+        # check SSL running OK
+        cert = env.get_cert(long_domain)
+        assert long_domain in cert.get_san_list()
+
+    # test case: fourth level domain
+    def test_md_702_070(self, env):
+        domain = self.test_domain
+        name_a = "one.test." + domain
+        name_b = "two.test." + domain
+        domains = [name_a, name_b]
+        #
+        # generate 1 MD and 2 vhosts
+        conf = MDConf(env)
+        conf.add_admin("admin@" + domain)
+        conf.add_md(domains)
+        conf.add_vhost(name_a)
+        conf.install()
+        #
+        # restart (-> drive), check that MD was synched and completes
+        assert env.apache_restart() == 0
+        assert env.await_completion(domains)
+        env.check_md_complete(domains[0])
+
+    # test case: fifth level domain
+    def test_md_702_071(self, env):
+        domain = self.test_domain
+        name_a = "one.more.test." + domain
+        name_b = "two.more.test." + domain
+        domains = [name_a, name_b]
+        #
+        # generate 1 MD and 2 vhosts
+        conf = MDConf(env)
+        conf.add_admin("admin@" + domain)
+        conf.add_md(domains)
+        conf.add_vhost(name_a)
+        conf.install()
+        #
+        # restart (-> drive), check that MD was synched and completes
+        assert env.apache_restart() == 0
+        assert env.await_completion(domains)
+        env.check_md_complete(domains[0])
+

Added: httpd/httpd/trunk/test/modules/md/test_720_wildcard.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_720_wildcard.py?rev=1894611&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_720_wildcard.py (added)
+++ httpd/httpd/trunk/test/modules/md/test_720_wildcard.py Fri Oct 29 10:05:29 2021
@@ -0,0 +1,233 @@
+# test wildcard certifcates
+import os
+
+import pytest
+
+from .md_conf import MDConf, MDConf
+from .md_env import MDTestEnv
+
+
+@pytest.mark.skipif(condition=not MDTestEnv.has_acme_server(),
+                    reason="no ACME test server configured")
+class TestWildcard:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env, acme):
+        env.APACHE_CONF_SRC = "data/test_auto"
+        acme.start(config='default')
+        env.check_acme()
+        env.clear_store()
+        MDConf(env).install()
+        assert env.apache_restart() == 0
+
+    @pytest.fixture(autouse=True, scope='function')
+    def _method_scope(self, env, request):
+        env.clear_store()
+        self.test_domain = env.get_request_domain(request)
+
+    # -----------------------------------------------------------------------------------------------
+    # test case: a wildcard certificate with ACMEv2, no dns-01 supported
+    #
+    def test_md_720_001(self, env):
+        domain = self.test_domain
+        
+        # generate config with DNS wildcard
+        domains = [domain, "*." + domain]
+        conf = MDConf(env)
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+
+        # restart, check that md is in store
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        # await drive completion
+        md = env.await_error(domain)
+        assert md
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'challenge-mismatch'
+
+    # -----------------------------------------------------------------------------------------------
+    # test case: a wildcard certificate with ACMEv2, only dns-01 configured, invalid command path 
+    #
+    def test_md_720_002(self, env):
+        dns01cmd = os.path.join(env.test_dir, "../modules/md/dns01-not-found.py")
+
+        domain = self.test_domain
+        domains = [domain, "*." + domain]
+        
+        conf = MDConf(env)
+        conf.add("MDCAChallenges dns-01")
+        conf.add(f"MDChallengeDns01 {dns01cmd}")
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+
+        # restart, check that md is in store
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        # await drive completion
+        md = env.await_error(domain)
+        assert md
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'challenge-setup-failure'
+
+    # variation, invalid cmd path, other challenges still get certificate for non-wildcard
+    def test_md_720_002b(self, env):
+        dns01cmd = os.path.join(env.test_dir, "../modules/md/dns01-not-found.py")
+        domain = self.test_domain
+        domains = [domain, "xxx." + domain]
+        
+        conf = MDConf(env)
+        conf.add(f"MDChallengeDns01 {dns01cmd}")
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+
+        # restart, check that md is in store
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        # await drive completion
+        assert env.await_completion([domain])
+        env.check_md_complete(domain)
+        # check: SSL is running OK
+        cert_a = env.get_cert(domain)
+        altnames = cert_a.get_san_list()
+        for domain in domains:
+            assert domain in altnames
+
+    # -----------------------------------------------------------------------------------------------
+    # test case: a wildcard certificate with ACMEv2, only dns-01 configured, invalid command option 
+    #
+    def test_md_720_003(self, env):
+        dns01cmd = os.path.join(env.test_dir, "../modules/md/dns01.py fail")
+        domain = self.test_domain
+        domains = [domain, "*." + domain]
+        
+        conf = MDConf(env)
+        conf.add("MDCAChallenges dns-01")
+        conf.add(f"MDChallengeDns01 {dns01cmd}")
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+
+        # restart, check that md is in store
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        # await drive completion
+        md = env.await_error(domain)
+        assert md
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'challenge-setup-failure'
+
+    # -----------------------------------------------------------------------------------------------
+    # test case: a wildcard name certificate with ACMEv2, only dns-01 configured 
+    #
+    def test_md_720_004(self, env):
+        dns01cmd = os.path.join(env.test_dir, "../modules/md/dns01.py")
+        domain = self.test_domain
+        domains = [domain, "*." + domain]
+        
+        conf = MDConf(env)
+        conf.add("MDCAChallenges dns-01")
+        conf.add(f"MDChallengeDns01 {dns01cmd}")
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+
+        # restart, check that md is in store
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        # await drive completion
+        assert env.await_completion([domain])
+        env.check_md_complete(domain)
+        # check: SSL is running OK
+        cert_a = env.get_cert(domain)
+        altnames = cert_a.get_san_list()
+        for domain in domains:
+            assert domain in altnames
+
+    # -----------------------------------------------------------------------------------------------
+    # test case: a wildcard name and 2nd normal vhost, not overlapping
+    #
+    def test_md_720_005(self, env):
+        dns01cmd = os.path.join(env.test_dir, "../modules/md/dns01.py")
+        domain = self.test_domain
+        domain2 = "www.x" + domain
+        domains = [domain, "*." + domain, domain2]
+        
+        conf = MDConf(env)
+        conf.add("MDCAChallenges dns-01")
+        conf.add(f"MDChallengeDns01 {dns01cmd}")
+        conf.add_md(domains)
+        conf.add_vhost(domain2)
+        conf.add_vhost(domains)
+        conf.install()
+
+        # restart, check that md is in store
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        # await drive completion
+        assert env.await_completion([domain])
+        env.check_md_complete(domain)
+        # check: SSL is running OK
+        cert_a = env.get_cert(domain)
+        altnames = cert_a.get_san_list()
+        for domain in domains:
+            assert domain in altnames
+
+    # -----------------------------------------------------------------------------------------------
+    # test case: a wildcard name and 2nd normal vhost, overlapping
+    def test_md_720_006(self, env):
+        dns01cmd = os.path.join(env.test_dir, "../modules/md/dns01.py")
+        domain = self.test_domain
+        dwild = "*." + domain
+        domain2 = "www." + domain
+        domains = [domain, dwild, domain2]
+        
+        conf = MDConf(env)
+        conf.add("MDCAChallenges dns-01")
+        conf.add(f"MDChallengeDns01 {dns01cmd}")
+        conf.add_md(domains)
+        conf.add_vhost(domain2)
+        conf.add_vhost([domain, dwild])
+        conf.install()
+
+        # restart, check that md is in store
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        # await drive completion
+        assert env.await_completion([domain])
+        env.check_md_complete(domain)
+        # check: SSL is running OK
+        cert_a = env.get_cert(domain)
+        altnames = cert_a.get_san_list()
+        for domain in [domain, dwild]:
+            assert domain in altnames
+
+    # -----------------------------------------------------------------------------------------------
+    # test case: a MDomain with just a wildcard, see #239
+    def test_md_720_007(self, env):
+        dns01cmd = os.path.join(env.test_dir, "../modules/md/dns01.py")
+        domain = self.test_domain
+        dwild = "*." + domain
+        wwwdomain = "www." + domain
+        domains = [dwild]
+
+        conf = MDConf(env)
+        conf.add("MDCAChallenges dns-01")
+        conf.add(f"MDChallengeDns01 {dns01cmd}")
+        conf.add_md(domains)
+        conf.add_vhost(wwwdomain)
+        conf.install()
+
+        # restart, check that md is in store
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        # await drive completion
+        assert env.await_completion([wwwdomain])
+        env.check_md_complete(dwild)
+        # check: SSL is running OK
+        cert_a = env.get_cert(wwwdomain)
+        altnames = cert_a.get_san_list()
+        assert domains == altnames

Added: httpd/httpd/trunk/test/modules/md/test_730_static.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_730_static.py?rev=1894611&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_730_static.py (added)
+++ httpd/httpd/trunk/test/modules/md/test_730_static.py Fri Oct 29 10:05:29 2021
@@ -0,0 +1,117 @@
+import os
+
+import pytest
+
+from .md_conf import MDConf
+from .md_env import MDTestEnv
+
+
+@pytest.mark.skipif(condition=not MDTestEnv.has_acme_server(),
+                    reason="no ACME test server configured")
+class TestStatic:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env, acme):
+        env.APACHE_CONF_SRC = "data/test_auto"
+        acme.start(config='default')
+        env.check_acme()
+        env.clear_store()
+        MDConf(env).install()
+        assert env.apache_restart() == 0
+
+    @pytest.fixture(autouse=True, scope='function')
+    def _method_scope(self, env, request):
+        env.clear_store()
+        self.test_domain = env.get_request_domain(request)
+
+    def test_md_730_001(self, env):
+        # MD with static cert files, will not be driven
+        domain = self.test_domain
+        domains = [domain, 'www.%s' % domain]
+        testpath = os.path.join(env.gen_dir, 'test_920_001')
+        # cert that is only 10 more days valid
+        env.create_self_signed_cert(domains, {"notBefore": -80, "notAfter": 10},
+                                    serial=730001, path=testpath)
+        cert_file = os.path.join(testpath, 'pubcert.pem')
+        pkey_file = os.path.join(testpath, 'privkey.pem')
+        assert os.path.exists(cert_file)
+        assert os.path.exists(pkey_file)
+        conf = MDConf(env)
+        conf.start_md(domains)
+        conf.add(f"MDCertificateFile {cert_file}")
+        conf.add(f"MDCertificateKeyFile {pkey_file}")
+        conf.end_md()
+        conf.add_vhost(domain)
+        conf.install()
+        assert env.apache_restart() == 0
+        
+        # check if the domain uses it, it appears in our stats and renewal is off
+        cert = env.get_cert(domain)
+        assert cert.same_serial_as(730001)
+        stat = env.get_md_status(domain)
+        assert stat
+        assert 'cert' in stat
+        assert stat['renew'] is True
+        assert 'renewal' not in stat
+
+    def test_md_730_002(self, env):
+        # MD with static cert files, force driving
+        domain = self.test_domain
+        domains = [domain, 'www.%s' % domain]
+        testpath = os.path.join(env.gen_dir, 'test_920_001')
+        # cert that is only 10 more days valid
+        env.create_self_signed_cert(domains, {"notBefore": -80, "notAfter": 10},
+                                    serial=730001, path=testpath)
+        cert_file = os.path.join(testpath, 'pubcert.pem')
+        pkey_file = os.path.join(testpath, 'privkey.pem')
+        assert os.path.exists(cert_file)
+        assert os.path.exists(pkey_file)
+        conf = MDConf(env)
+        conf.start_md(domains)
+        conf.add(f"MDPrivateKeys secp384r1 rsa3072")
+        conf.add(f"MDCertificateFile {cert_file}")
+        conf.add(f"MDCertificateKeyFile {pkey_file}")
+        conf.add("MDRenewMode always")
+        conf.end_md()
+        conf.add_vhost(domain)
+        conf.install()
+        assert env.apache_restart() == 0
+        # this should enforce a renewal
+        stat = env.get_md_status(domain)
+        assert stat['renew'] is True, stat
+        assert env.await_completion(domains, restart=False)
+        # and show the newly created certificates
+        stat = env.get_md_status(domain)
+        assert 'renewal' in stat
+        assert 'cert' in stat['renewal']
+        assert 'secp384r1' in stat['renewal']['cert']
+        assert 'rsa' in stat['renewal']['cert']
+
+    def test_md_730_003(self, env):
+        # just configuring one file will not work
+        domain = self.test_domain
+        domains = [domain, 'www.%s' % domain]
+        testpath = os.path.join(env.gen_dir, 'test_920_001')
+        # cert that is only 10 more days valid
+        env.create_self_signed_cert(domains, {"notBefore": -80, "notAfter": 10},
+                                    serial=730001, path=testpath)
+        cert_file = os.path.join(testpath, 'pubcert.pem')
+        pkey_file = os.path.join(testpath, 'privkey.pem')
+        assert os.path.exists(cert_file)
+        assert os.path.exists(pkey_file)
+        
+        conf = MDConf(env)
+        conf.start_md(domains)
+        conf.add(f"MDCertificateFile {cert_file}")
+        conf.end_md()
+        conf.add_vhost(domain)
+        conf.install()
+        assert env.apache_fail() == 0
+        
+        conf = MDConf(env)
+        conf.start_md(domains)
+        conf.add(f"MDCertificateKeyFile {pkey_file}")
+        conf.end_md()
+        conf.add_vhost(domain)
+        conf.install()
+        assert env.apache_fail() == 0

Added: httpd/httpd/trunk/test/modules/md/test_740_acme_errors.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_740_acme_errors.py?rev=1894611&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_740_acme_errors.py (added)
+++ httpd/httpd/trunk/test/modules/md/test_740_acme_errors.py Fri Oct 29 10:05:29 2021
@@ -0,0 +1,72 @@
+# test ACME error responses and their processing
+import pytest
+
+from .md_conf import MDConf
+from .md_env import MDTestEnv
+
+
+@pytest.mark.skipif(condition=not MDTestEnv.has_acme_server(),
+                    reason="no ACME test server configured")
+class TestAcmeErrors:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env, acme):
+        env.APACHE_CONF_SRC = "data/test_auto"
+        acme.start(config='default')
+        env.check_acme()
+        env.clear_store()
+        MDConf(env).install()
+        assert env.apache_restart() == 0
+
+    @pytest.fixture(autouse=True, scope='function')
+    def _method_scope(self, env, request):
+        env.clear_store()
+        self.test_domain = env.get_request_domain(request)
+
+    # -----------------------------------------------------------------------------------------------
+    # test case: MD with 2 names, one invalid
+    #
+    def test_md_740_000(self, env):
+        domain = self.test_domain
+        domains = [domain, "invalid!." + domain]
+        conf = MDConf(env)
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        md = env.await_error(domain)
+        assert md
+        assert md['renewal']['errors'] > 0
+        if env.acme_server == 'pebble':
+            assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:malformed'
+            assert md['renewal']['last']['detail'] == \
+                   "Order included DNS identifier with a value containing an illegal character: '!'"
+        else:
+            assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:rejectedIdentifier'
+            assert md['renewal']['last']['detail'] == (
+                    "Error creating new order :: Cannot issue for "
+                    "\"%s\": Domain name contains an invalid character" % domains[1])
+
+    # test case: MD with 3 names, 2 invalid
+    #
+    def test_md_740_001(self, env):
+        domain = self.test_domain
+        domains = [domain, "invalid1!." + domain, "invalid2!." + domain]
+        conf = MDConf(env)
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        md = env.await_error(domain)
+        assert md
+        assert md['renewal']['errors'] > 0
+        if env.acme_server == 'pebble':
+            assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:malformed'
+            assert md['renewal']['last']['detail'].startswith(
+                "Order included DNS identifier with a value containing an illegal character")
+        else:
+            assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:rejectedIdentifier'
+            assert md['renewal']['last']['detail'].startswith(
+                "Error creating new order :: Cannot issue for")
+            assert md['renewal']['last']['subproblems']
+            assert len(md['renewal']['last']['subproblems']) == 2

Added: httpd/httpd/trunk/test/modules/md/test_741_setup_errors.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_741_setup_errors.py?rev=1894611&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_741_setup_errors.py (added)
+++ httpd/httpd/trunk/test/modules/md/test_741_setup_errors.py Fri Oct 29 10:05:29 2021
@@ -0,0 +1,48 @@
+# test ACME error responses and their processing
+import os
+
+import pytest
+
+from .md_conf import MDConf
+from .md_env import MDTestEnv
+
+
+@pytest.mark.skipif(condition=not MDTestEnv.has_acme_server(),
+                    reason="no ACME test server configured")
+class TestSetupErrors:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env, acme):
+        env.APACHE_CONF_SRC = "data/test_auto"
+        acme.start(config='default')
+        env.check_acme()
+        env.clear_store()
+        MDConf(env).install()
+        assert env.apache_restart() == 0
+
+    @pytest.fixture(autouse=True, scope='function')
+    def _method_scope(self, env, request):
+        env.clear_store()
+        self.mcmd = os.path.join(env.test_dir, "../modules/md/http_challenge_foobar.py")
+        self.test_domain = env.get_request_domain(request)
+
+    def test_md_741_001(self, env):
+        # setup an MD with a MDMessageCmd that make the http-01 challenge file invalid
+        # before the ACME server is asked to retrieve it. This will result in
+        # an "invalid" domain authorization.
+        # The certificate sign-up will be attempted again after 4 seconds and
+        # of course fail again.
+        # Verify that the error counter for the staging job increments, so
+        # that our retry logic goes into proper delayed backoff.
+        domain = self.test_domain
+        domains = [domain]
+        conf = MDConf(env)
+        conf.add("MDCAChallenges http-01")
+        conf.add(f"MDMessageCmd {self.mcmd} {env.store_dir}")
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        md = env.await_error(domain, errors=2, timeout=10)
+        assert md
+        assert md['renewal']['errors'] > 0

Added: httpd/httpd/trunk/test/modules/md/test_750_eab.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_750_eab.py?rev=1894611&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_750_eab.py (added)
+++ httpd/httpd/trunk/test/modules/md/test_750_eab.py Fri Oct 29 10:05:29 2021
@@ -0,0 +1,239 @@
+import pytest
+
+from .md_conf import MDConf
+from .md_env import MDTestEnv
+
+
+@pytest.mark.skipif(condition=not MDTestEnv.has_acme_eab(),
+                    reason="ACME test server does not support External Account Binding")
+class TestEab:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env, acme):
+        acme.start(config='eab')
+        env.check_acme()
+        env.clear_store()
+        MDConf(env).install()
+        assert env.apache_restart() == 0
+
+    @pytest.fixture(autouse=True, scope='function')
+    def _method_scope(self, env, request):
+        env.clear_store()
+        self.test_domain = env.get_request_domain(request)
+
+    def test_md_750_001(self, env):
+        # md without EAB configured
+        domain = self.test_domain
+        domains = [domain]
+        conf = MDConf(env)
+        conf.add_md(domains)
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        md = env.await_error(domain)
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'
+
+    def test_md_750_002(self, env):
+        # md with known EAB KID and non base64 hmac key configured
+        domain = self.test_domain
+        domains = [domain]
+        conf = MDConf(env)
+        conf.add("MDExternalAccountBinding kid-1 äöüß")
+        conf.add_md(domains)
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        md = env.await_error(domain)
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'apache:eab-hmac-invalid'
+
+    def test_md_750_003(self, env):
+        # md with empty EAB KID configured
+        domain = self.test_domain
+        domains = [domain]
+        conf = MDConf(env)
+        conf.add("MDExternalAccountBinding \" \" bm90IGEgdmFsaWQgaG1hYwo=")
+        conf.add_md(domains)
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        md = env.await_error(domain)
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:unauthorized'
+
+    def test_md_750_004(self, env):
+        # md with unknown EAB KID configured
+        domain = self.test_domain
+        domains = [domain]
+        conf = MDConf(env)
+        conf.add("MDExternalAccountBinding key-x bm90IGEgdmFsaWQgaG1hYwo=")
+        conf.add_md(domains)
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        md = env.await_error(domain)
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:unauthorized'
+
+    def test_md_750_005(self, env):
+        # md with known EAB KID but wrong HMAC configured
+        domain = self.test_domain
+        domains = [domain]
+        conf = MDConf(env)
+        conf.add("MDExternalAccountBinding kid-1 bm90IGEgdmFsaWQgaG1hYwo=")
+        conf.add_md(domains)
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        md = env.await_error(domain)
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:unauthorized'
+
+    def test_md_750_010(self, env):
+        # md with correct EAB configured
+        domain = self.test_domain
+        domains = [domain]
+        conf = MDConf(env)
+        # this is one of the values in conf/pebble-eab.json
+        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
+        conf.add_md(domains)
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion(domains)
+
+    def test_md_750_011(self, env):
+        # first one md with EAB, then one without, works only for the first
+        # as the second is unable to reuse the account
+        domain_a = f"a{self.test_domain}"
+        domain_b = f"b{self.test_domain}"
+        conf = MDConf(env)
+        conf.start_md([domain_a])
+        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
+        conf.end_md()
+        conf.add_vhost(domains=[domain_a])
+        conf.add_md([domain_b])
+        conf.add_vhost(domains=[domain_b])
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion([domain_a], restart=False)
+        md = env.await_error(domain_b)
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'
+
+    def test_md_750_012(self, env):
+        # first one md without EAB, then one with
+        # first one fails, second works
+        domain_a = f"a{self.test_domain}"
+        domain_b = f"b{self.test_domain}"
+        conf = MDConf(env)
+        conf.add_md([domain_a])
+        conf.add_vhost(domains=[domain_a])
+        conf.start_md([domain_b])
+        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
+        conf.end_md()
+        conf.add_vhost(domains=[domain_b])
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion([domain_b], restart=False)
+        md = env.await_error(domain_a)
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'
+
+    def test_md_750_013(self, env):
+        # 2 mds with the same EAB, should one create a single account
+        domain_a = f"a{self.test_domain}"
+        domain_b = f"b{self.test_domain}"
+        conf = MDConf(env)
+        conf.start_md([domain_a])
+        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
+        conf.end_md()
+        conf.add_vhost(domains=[domain_a])
+        conf.start_md([domain_b])
+        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
+        conf.end_md()
+        conf.add_vhost(domains=[domain_b])
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion([domain_a, domain_b])
+        md_a = env.get_md_status(domain_a)
+        md_b = env.get_md_status(domain_b)
+        assert md_a['ca'] == md_b['ca']
+
+    def test_md_750_014(self, env):
+        # md with correct EAB, get cert, change to another correct EAB
+        # needs to create a new account
+        domain = self.test_domain
+        domains = [domain]
+        conf = MDConf(env)
+        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
+        conf.add_md(domains)
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion(domains)
+        md_1 = env.get_md_status(domain)
+        conf = MDConf(env)
+        # this is another one of the values in conf/pebble-eab.json
+        # add a dns name to force renewal
+        domains = [domain, f'www.{domain}']
+        conf.add("MDExternalAccountBinding kid-2 b10lLJs8l1GPIzsLP0s6pMt8O0XVGnfTaCeROxQM0BIt2XrJMDHJZBM5NuQmQJQH")
+        conf.add_md(domains)
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion(domains)
+        md_2 = env.get_md_status(domain)
+        assert md_1['ca'] != md_2['ca']
+
+    def test_md_750_015(self, env):
+        # md with correct EAB, get cert, change to no EAB
+        # needs to fail
+        domain = self.test_domain
+        domains = [domain]
+        conf = MDConf(env)
+        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
+        conf.add_md(domains)
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion(domains)
+        conf = MDConf(env)
+        # this is another one of the values in conf/pebble-eab.json
+        # add a dns name to force renewal
+        domains = [domain, f'www.{domain}']
+        conf.add_md(domains)
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_error(domain)
+        md = env.await_error(domain)
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'
+
+    def test_md_750_016(self, env):
+        # md with correct EAB, get cert, change to invalid EAB
+        # needs to fail
+        domain = self.test_domain
+        domains = [domain]
+        conf = MDConf(env)
+        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
+        conf.add_md(domains)
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion(domains)
+        conf = MDConf(env)
+        # this is another one of the values in conf/pebble-eab.json
+        # add a dns name to force renewal
+        domains = [domain, f'www.{domain}']
+        conf.add("MDExternalAccountBinding kid-invalud blablabalbalbla")
+        conf.add_md(domains)
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_error(domain)
+        md = env.await_error(domain)
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:unauthorized'

Added: httpd/httpd/trunk/test/modules/md/test_751_sectigo.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_751_sectigo.py?rev=1894611&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_751_sectigo.py (added)
+++ httpd/httpd/trunk/test/modules/md/test_751_sectigo.py Fri Oct 29 10:05:29 2021
@@ -0,0 +1,181 @@
+import os
+import re
+import time
+
+import pytest
+
+from .md_conf import MDConf
+
+# set the environment variables
+#   SECTIGO_EAB="$kid $hmac" for
+#   SECTIGO_TLD="<your registered dns name>"
+# these tests to become active
+#
+
+DEMO_ACME = "https://acme.demo.sectigo.com/"
+DEMO_TLD = None
+
+EABS = [
+    {'kid': '0123', 'hmac': 'abcdef'},
+]
+
+
+def missing_eab():
+    global EABS
+    if len(EABS) == 1 and 'SECTIGO_EAB' in os.environ:
+        m = re.match(r'^\s*(\S+)\s+(\S+)\s*$', os.environ['SECTIGO_EAB'])
+        if m:
+            EABS.append({'kid': m.group(1), 'hmac': m.group(2)})
+    return len(EABS) == 1
+
+
+def missing_tld():
+    global DEMO_TLD
+    if 'SECTIGO_TLD' in os.environ:
+        DEMO_TLD = os.environ['SECTIGO_TLD']
+    return DEMO_TLD is None
+
+
+@pytest.mark.skipif(condition=missing_tld(), reason="env var SECTIGO_TLD not set")
+@pytest.mark.skipif(condition=missing_eab(), reason="env var SECTIGO_EAB not set")
+class TestSectigo:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env, acme):
+        acme.start(config='eab')
+        env.check_acme()
+        env.clear_store()
+        MDConf(env).install()
+        assert env.apache_restart() == 0
+
+    @pytest.fixture(autouse=True, scope='function')
+    def _method_scope(self, env, request):
+        env.clear_store()
+        self.test_domain = env.get_request_domain(request)
+
+    def test_md_751_001(self, env):
+        # valid config, expect cert with correct chain
+        domain = f"test1.{DEMO_TLD}"
+        domains = [domain]
+        conf = MDConf(env)
+        conf.start_md(domains)
+        conf.add(f"MDCertificateAuthority {DEMO_ACME}")
+        conf.add("MDCACertificateFile none")
+        conf.add(f"MDExternalAccountBinding {EABS[1]['kid']} {EABS[1]['hmac']}")
+        conf.end_md()
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion(domains)
+        r = env.curl_get(f"https://{domain}:{env.https_port}", options=[
+            "--cacert", f"{env.test_dir}/data/sectigo-demo-root.pem"
+        ])
+        assert r.response['status'] == 200
+
+    def test_md_751_002(self, env):
+        # without EAB set
+        domain = f"test1.{DEMO_TLD}"
+        domains = [domain]
+        conf = MDConf(env)
+        conf.start_md(domains)
+        conf.add(f"MDCertificateAuthority {DEMO_ACME}")
+        conf.add("MDCACertificateFile none")
+        conf.end_md()
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_error(domain)
+        md = env.get_md_status(domain)
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'
+
+    def test_md_751_003(self, env):
+        # with wrong EAB set
+        domain = f"test1.{DEMO_TLD}"
+        domains = [domain]
+        conf = MDConf(env)
+        conf.start_md(domains)
+        conf.add(f"MDCertificateAuthority {DEMO_ACME}")
+        conf.add("MDCACertificateFile none")
+        conf.add(f"MDExternalAccountBinding xxxxxx aaaaaaaaaaaaasdddddsdasdsadsadsadasdsadsa")
+        conf.end_md()
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_error(domain)
+        md = env.get_md_status(domain)
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:unauthorized'
+
+    def test_md_751_004(self, env):
+        # valid config, get cert, add dns name, renew cert
+        domain = f"test1.{DEMO_TLD}"
+        domain2 = f"test2.{DEMO_TLD}"
+        domains = [domain]
+        conf = MDConf(env)
+        conf.start_md(domains)
+        conf.add(f"MDCertificateAuthority {DEMO_ACME}")
+        conf.add("MDCACertificateFile none")
+        conf.add(f"MDExternalAccountBinding {EABS[1]['kid']} {EABS[1]['hmac']}")
+        conf.end_md()
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion(domains)
+        r = env.curl_get(f"https://{domain}:{env.https_port}", options=[
+            "--cacert", f"{env.test_dir}/data/sectigo-demo-root.pem"
+        ])
+        assert r.response['status'] == 200
+        r = env.curl_get(f"https://{domain2}:{env.https_port}", options=[
+            "--cacert", f"{env.test_dir}/data/sectigo-demo-root.pem"
+        ])
+        assert r.exit_code != 0
+        md1 = env.get_md_status(domain)
+        acct1 = md1['ca']['account']
+        # add the domain2 to the dns names
+        domains = [domain, domain2]
+        conf = MDConf(env)
+        conf.start_md(domains)
+        conf.add(f"MDCertificateAuthority {DEMO_ACME}")
+        conf.add("MDCACertificateFile none")
+        conf.add(f"MDExternalAccountBinding {EABS[1]['kid']} {EABS[1]['hmac']}")
+        conf.end_md()
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion(domains)
+        r = env.curl_get(f"https://{domain2}:{env.https_port}", options=[
+            "--cacert", f"{env.test_dir}/data/sectigo-demo-root.pem"
+        ])
+        assert r.response['status'] == 200
+        md2 = env.get_md_status(domain)
+        acct2 = md2['ca']['account']
+        assert acct2 == acct1, f"ACME account was not reused: {acct1} became {acct2}"
+
+    def test_md_751_020(self, env):
+        # valid config, get cert, check OCSP status
+        domain = f"test1.{DEMO_TLD}"
+        domains = [domain]
+        conf = MDConf(env)
+        conf.add("MDStapling on")
+        conf.start_md(domains)
+        conf.add(f"""
+            MDCertificateAuthority {DEMO_ACME}
+            MDCACertificateFile none
+            MDExternalAccountBinding {EABS[1]['kid']} {EABS[1]['hmac']}
+            """)
+        conf.end_md()
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion(domains)
+        r = env.curl_get(f"https://{domain}:{env.https_port}", options=[
+            "--cacert", f"{env.test_dir}/data/sectigo-demo-root.pem"
+        ])
+        assert r.response['status'] == 200
+        time.sleep(1)
+        for domain in domains:
+            stat = env.await_ocsp_status(domain,
+                                         ca_file=f"{env.test_dir}/data/sectigo-demo-root.pem")
+            assert stat['ocsp'] == "successful (0x0)"
+            assert stat['verify'] == "0 (ok)"

Added: httpd/httpd/trunk/test/modules/md/test_752_zerossl.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_752_zerossl.py?rev=1894611&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_752_zerossl.py (added)
+++ httpd/httpd/trunk/test/modules/md/test_752_zerossl.py Fri Oct 29 10:05:29 2021
@@ -0,0 +1,202 @@
+import os
+import time
+
+import pytest
+
+from .md_conf import MDConf
+
+# set the environment variables
+#   ZEROSSL_TLD="<your registered dns name>"
+# these tests to become active
+#
+
+DEMO_ACME = "https://acme.zerossl.com/v2/DV90"
+DEMO_EAB_URL = "http://api.zerossl.com/acme/eab-credentials-email"
+DEMO_TLD = None
+
+
+def missing_tld():
+    global DEMO_TLD
+    if 'ZEROSSL_TLD' in os.environ:
+        DEMO_TLD = os.environ['ZEROSSL_TLD']
+    return DEMO_TLD is None
+
+
+def get_new_eab(env):
+    r = env.curl_raw(DEMO_EAB_URL, options=[
+        "-d", f"email=admin@zerossl.{DEMO_TLD}"
+    ], force_resolve=False)
+    assert r.exit_code == 0
+    assert r.json
+    assert r.json['success'] is True
+    assert r.json['eab_kid']
+    assert r.json['eab_hmac_key']
+    return {'kid': r.json['eab_kid'], 'hmac': r.json['eab_hmac_key']}
+
+
+@pytest.mark.skipif(condition=missing_tld(), reason="env var ZEROSSL_TLD not set")
+class TestZeroSSL:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env, acme):
+        acme.start(config='eab')
+        env.check_acme()
+        env.clear_store()
+        MDConf(env).install()
+        assert env.apache_restart() == 0
+
+    @pytest.fixture(autouse=True, scope='function')
+    def _method_scope(self, env, request):
+        self.test_domain = env.get_request_domain(request)
+
+    def test_md_752_001(self, env):
+        # valid config, expect cert with correct chain
+        domain = f"test1.{DEMO_TLD}"
+        domains = [domain]
+        eab = get_new_eab(env)
+        conf = MDConf(env)
+        conf.start_md(domains)
+        conf.add(f"""
+            MDCertificateAuthority {DEMO_ACME}
+            MDCertificateAgreement accepted
+            MDContactEmail admin@zerossl.{DEMO_TLD}
+            MDCACertificateFile none
+            MDExternalAccountBinding {eab['kid']} {eab['hmac']}
+        """)
+        conf.end_md()
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion(domains)
+        r = env.curl_get(f"https://{domain}:{env.https_port}", options=[
+            "--cacert", f"{env.test_dir}/data/sectigo-demo-root.pem"
+        ])
+        assert r.response['status'] == 200
+
+    def test_md_752_002(self, env):
+        # without EAB set
+        domain = f"test1.{DEMO_TLD}"
+        domains = [domain]
+        conf = MDConf(env)
+        conf.start_md(domains)
+        conf.add(f"""
+            MDCertificateAuthority {DEMO_ACME}
+            MDCertificateAgreement accepted
+            MDContactEmail admin@zerossl.{DEMO_TLD}
+            MDCACertificateFile none
+        """)
+        conf.end_md()
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_error(domain)
+        md = env.get_md_status(domain)
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'
+
+    def test_md_752_003(self, env):
+        # with wrong EAB set
+        domain = f"test1.{DEMO_TLD}"
+        domains = [domain]
+        conf = MDConf(env)
+        conf.start_md(domains)
+        conf.add(f"""
+            MDCertificateAuthority {DEMO_ACME}
+            MDCertificateAgreement accepted
+            MDContactEmail admin@zerossl.{DEMO_TLD}
+            MDCACertificateFile none
+        """)
+        conf.add(f"MDExternalAccountBinding YmxhYmxhYmxhCg YmxhYmxhYmxhCg")
+        conf.end_md()
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_error(domain)
+        md = env.get_md_status(domain)
+        assert md['renewal']['errors'] > 0
+        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:malformed'
+
+    def test_md_752_004(self, env):
+        # valid config, get cert, add dns name, renew cert
+        domain = f"test1.{DEMO_TLD}"
+        domain2 = f"test2.{DEMO_TLD}"
+        domains = [domain]
+        eab = get_new_eab(env)
+        conf = MDConf(env)
+        conf.start_md(domains)
+        conf.add(f"""
+            MDCertificateAuthority {DEMO_ACME}
+            MDCertificateAgreement accepted
+            MDContactEmail admin@zerossl.{DEMO_TLD}
+            MDCACertificateFile none
+            MDExternalAccountBinding {eab['kid']} {eab['hmac']}
+        """)
+        conf.end_md()
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion(domains)
+        r = env.curl_get(f"https://{domain}:{env.https_port}", options=[
+            "--cacert", f"{env.test_dir}/data/sectigo-demo-root.pem"
+        ])
+        assert r.response['status'] == 200
+        r = env.curl_get(f"https://{domain2}:{env.https_port}", options=[
+            "--cacert", f"{env.test_dir}/data/sectigo-demo-root.pem"
+        ])
+        assert r.exit_code != 0
+        md1 = env.get_md_status(domain)
+        acct1 = md1['ca']['account']
+        # add the domain2 to the dns names
+        domains = [domain, domain2]
+        conf = MDConf(env)
+        conf.start_md(domains)
+        conf.add(f"""
+            MDCertificateAuthority {DEMO_ACME}
+            MDCertificateAgreement accepted
+            MDContactEmail admin@zerossl.{DEMO_TLD}
+            MDCACertificateFile none
+            MDExternalAccountBinding {eab['kid']} {eab['hmac']}
+        """)
+        conf.end_md()
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion(domains)
+        r = env.curl_get(f"https://{domain2}:{env.https_port}", options=[
+            "--cacert", f"{env.test_dir}/data/sectigo-demo-root.pem"
+        ])
+        assert r.response['status'] == 200
+        md2 = env.get_md_status(domain)
+        acct2 = md2['ca']['account']
+        assert acct2 == acct1, f"ACME account was not reused: {acct1} became {acct2}"
+
+    def test_md_752_020(self, env):
+        # valid config, get cert, check OCSP status
+        domain = f"test1.{DEMO_TLD}"
+        domains = [domain]
+        eab = get_new_eab(env)
+        conf = MDConf(env)
+        conf.add("MDStapling on")
+        conf.start_md(domains)
+        conf.add(f"""
+            MDCertificateAuthority {DEMO_ACME}
+            MDCertificateAgreement accepted
+            MDContactEmail admin@zerossl.{DEMO_TLD}
+            MDCACertificateFile none
+            MDExternalAccountBinding {eab['kid']} {eab['hmac']}
+        """)
+        conf.end_md()
+        conf.add_vhost(domains=domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.await_completion(domains)
+        r = env.curl_get(f"https://{domain}:{env.https_port}", options=[
+            "--cacert", f"{env.test_dir}/data/sectigo-demo-root.pem"
+        ])
+        assert r.response['status'] == 200
+        time.sleep(1)
+        for domain in domains:
+            stat = env.await_ocsp_status(domain,
+                                         ca_file=f"{env.test_dir}/data/sectigo-demo-root.pem")
+            assert stat['ocsp'] == "successful (0x0)"
+            assert stat['verify'] == "0 (ok)"

Added: httpd/httpd/trunk/test/modules/md/test_800_must_staple.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_800_must_staple.py?rev=1894611&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_800_must_staple.py (added)
+++ httpd/httpd/trunk/test/modules/md/test_800_must_staple.py Fri Oct 29 10:05:29 2021
@@ -0,0 +1,84 @@
+# test mod_md must-staple support
+import pytest
+
+from .md_conf import MDConf
+from .md_cert_util import MDCertUtil
+from .md_env import MDTestEnv
+
+
+@pytest.mark.skipif(condition=not MDTestEnv.has_acme_server(),
+                    reason="no ACME test server configured")
+class TestMustStaple:
+    domain = None
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env, acme):
+        acme.start(config='default')
+        env.check_acme()
+        env.clear_store()
+        MDConf(env).install()
+        assert env.apache_restart() == 0
+
+    @pytest.fixture(autouse=True, scope='function')
+    def _method_scope(self, env, request):
+        self.domain = env.get_class_domain(self.__class__)
+
+    def configure_httpd(self, env, domain, add_lines=""):
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add(add_lines)
+        conf.add_md([domain])
+        conf.add_vhost(domain)
+        conf.install()
+
+    # MD with default, e.g. not staple
+    def test_md_800_001(self, env):
+        self.configure_httpd(env, self.domain)
+        assert env.apache_restart() == 0
+        assert env.await_completion([self.domain])
+        env.check_md_complete(self.domain)
+        cert1 = MDCertUtil(env.store_domain_file(self.domain, 'pubcert.pem'))
+        assert not cert1.get_must_staple()
+
+    # MD that should explicitly not staple
+    def test_md_800_002(self, env):
+        self.configure_httpd(env, self.domain, "MDMustStaple off")
+        assert env.apache_restart() == 0
+        env.check_md_complete(self.domain)
+        cert1 = MDCertUtil(env.store_domain_file(self.domain, 'pubcert.pem'))
+        assert not cert1.get_must_staple()
+        stat = env.get_ocsp_status(self.domain)
+        assert stat['ocsp'] == "no response sent" 
+
+    # MD that must staple and toggle off again
+    @pytest.mark.skipif(MDTestEnv.lacks_ocsp(), reason="no OCSP responder")
+    def test_md_800_003(self, env):
+        self.configure_httpd(env, self.domain, "MDMustStaple on")
+        assert env.apache_restart() == 0
+        assert env.await_completion([self.domain])
+        env.check_md_complete(self.domain)
+        cert1 = MDCertUtil(env.store_domain_file(self.domain, 'pubcert.pem'))
+        assert cert1.get_must_staple()
+        self.configure_httpd(env, self.domain, "MDMustStaple off")
+        assert env.apache_restart() == 0
+        assert env.await_completion([self.domain])
+        env.check_md_complete(self.domain)
+        cert1 = MDCertUtil(env.store_domain_file(self.domain, 'pubcert.pem'))
+        assert not cert1.get_must_staple()
+
+    # MD that must staple
+    @pytest.mark.skipif(MDTestEnv.lacks_ocsp(), reason="no OCSP responder")
+    @pytest.mark.skipif(MDTestEnv.get_ssl_module() != "ssl", reason="only for mod_ssl")
+    def test_md_800_004(self, env):
+        # mod_ssl stapling is off, expect no stapling
+        stat = env.get_ocsp_status(self.domain)
+        assert stat['ocsp'] == "no response sent" 
+        # turn mod_ssl stapling on, expect an answer
+        self.configure_httpd(env, self.domain, """
+            LogLevel ssl:trace2
+            SSLUseStapling On
+            SSLStaplingCache shmcb:stapling_cache(128000)
+            """)
+        assert env.apache_restart() == 0
+        stat = env.get_ocsp_status(self.domain)
+        assert stat['ocsp'] == "successful (0x0)" 
+        assert stat['verify'] == "0 (ok)"