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 [3/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_310_conf_store.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_310_conf_store.py?rev=1894611&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_310_conf_store.py (added)
+++ httpd/httpd/trunk/test/modules/md/test_310_conf_store.py Fri Oct 29 10:05:29 2021
@@ -0,0 +1,850 @@
+# test mod_md basic configurations
+import time
+
+import pytest
+import os
+
+from .md_conf import MDConf
+from .md_env import MDTestEnv
+
+SEC_PER_DAY = 24 * 60 * 60
+MS_PER_DAY = SEC_PER_DAY * 1000
+NS_PER_DAY = MS_PER_DAY * 1000
+
+
+@pytest.mark.skipif(condition=not MDTestEnv.has_a2md(), reason="no a2md available")
+@pytest.mark.skipif(condition=not MDTestEnv.has_acme_server(),
+                    reason="no ACME test server configured")
+class TestConf:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env, acme):
+        acme.start(config='default')
+        env.check_acme()
+
+    @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: no md definitions in config
+    def test_md_310_001(self, env):
+        MDConf(env, text="").install()
+        assert env.apache_restart() == 0
+        r = env.a2md(["list"])
+        assert 0 == len(r.json["output"])
+
+    # test case: add md definitions on empty store
+    @pytest.mark.parametrize("confline,dns_lists,md_count", [
+        ("MDomain testdomain.org www.testdomain.org mail.testdomain.org", 
+            [["testdomain.org", "www.testdomain.org", "mail.testdomain.org"]], 1),
+        ("""MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            MDomain testdomain2.org www.testdomain2.org mail.testdomain2.org""", 
+            [["testdomain.org", "www.testdomain.org", "mail.testdomain.org"],
+             ["testdomain2.org", "www.testdomain2.org", "mail.testdomain2.org"]], 2)
+    ])
+    def test_md_310_100(self, env, confline, dns_lists, md_count):
+        MDConf(env, text=confline).install()
+        assert env.apache_restart() == 0
+        for i in range(0, len(dns_lists)):
+            env.check_md(dns_lists[i], state=1)
+
+    # test case: add managed domains as separate steps
+    def test_md_310_101(self, env):
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"], state=1)
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            MDomain testdomain2.org www.testdomain2.org mail.testdomain2.org
+            """).install()
+        assert env.apache_restart() == 0
+        env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"], state=1)
+        env.check_md(["testdomain2.org", "www.testdomain2.org", "mail.testdomain2.org"], state=1)
+
+    # test case: add dns to existing md
+    def test_md_310_102(self, env):
+        assert env.a2md(["add", "testdomain.org", "www.testdomain.org"]).exit_code == 0
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"], state=1)
+
+    # test case: add new md definition with acme url, acme protocol, acme agreement
+    def test_md_310_103(self, env):
+        MDConf(env, text="""
+            MDCertificateAuthority http://acme.test.org:4000/directory
+            MDCertificateProtocol ACME
+            MDCertificateAgreement http://acme.test.org:4000/terms/v1
+
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """, local_ca=False).install()
+        assert env.apache_restart() == 0
+        name = "testdomain.org"
+        env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
+                     ca="http://acme.test.org:4000/directory", protocol="ACME",
+                     agreement="http://acme.test.org:4000/terms/v1")
+
+    # test case: add to existing md: acme url, acme protocol
+    def test_md_310_104(self, env):
+        name = "testdomain.org"
+        MDConf(env, local_ca=False, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
+                     ca="https://acme-v02.api.letsencrypt.org/directory", protocol="ACME")
+        MDConf(env, local_ca=False, text="""
+            MDCertificateAuthority http://acme.test.org:4000/directory
+            MDCertificateProtocol ACME
+            MDCertificateAgreement http://acme.test.org:4000/terms/v1
+
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
+                     ca="http://acme.test.org:4000/directory", protocol="ACME",
+                     agreement="http://acme.test.org:4000/terms/v1")
+
+    # test case: add new md definition with server admin
+    def test_md_310_105(self, env):
+        MDConf(env, admin="admin@testdomain.org", text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        name = "testdomain.org"
+        env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
+                     contacts=["mailto:admin@testdomain.org"])
+
+    # test case: add to existing md: server admin
+    def test_md_310_106(self, env):
+        name = "testdomain.org"
+        assert env.a2md(["add", name, "www.testdomain.org", "mail.testdomain.org"]).exit_code == 0
+        MDConf(env, admin="admin@testdomain.org", text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
+                     contacts=["mailto:admin@testdomain.org"])
+
+    # test case: assign separate contact info based on VirtualHost
+    def test_md_310_107(self, env):
+        MDConf(env, admin="", text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            MDomain testdomain2.org www.testdomain2.org mail.testdomain2.org
+
+            <VirtualHost *:12346>
+                ServerName testdomain.org
+                ServerAlias www.testdomain.org
+                ServerAdmin mailto:admin@testdomain.org
+            </VirtualHost>
+
+            <VirtualHost *:12346>
+                ServerName testdomain2.org
+                ServerAlias www.testdomain2.org
+                ServerAdmin mailto:admin@testdomain2.org
+            </VirtualHost>
+            """).install()
+        assert env.apache_restart() == 0
+        name1 = "testdomain.org"
+        name2 = "testdomain2.org"
+        env.check_md([name1, "www." + name1, "mail." + name1], state=1, contacts=["mailto:admin@" + name1])
+        env.check_md([name2, "www." + name2, "mail." + name2], state=1, contacts=["mailto:admin@" + name2])
+
+    # test case: normalize names - lowercase
+    def test_md_310_108(self, env):
+        MDConf(env, text="""
+            MDomain testdomain.org WWW.testdomain.org MAIL.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"], state=1)
+
+    # test case: default drive mode - auto
+    def test_md_310_109(self, env):
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['renew-mode'] == 1
+
+    # test case: drive mode manual
+    def test_md_310_110(self, env):
+        MDConf(env, text="""
+            MDRenewMode manual
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['renew-mode'] == 0
+
+    # test case: drive mode auto
+    def test_md_310_111(self, env):
+        MDConf(env, text="""
+            MDRenewMode auto
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['renew-mode'] == 1
+
+    # test case: drive mode always
+    def test_md_310_112(self, env):
+        MDConf(env, text="""
+            MDRenewMode always
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['renew-mode'] == 2
+
+    # test case: renew window - 14 days
+    def test_md_310_113a(self, env):
+        MDConf(env, text="""
+            MDRenewWindow 14d
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['renew-window'] == '14d'
+
+    # test case: renew window - 10 percent
+    def test_md_310_113b(self, env):
+        MDConf(env, text="""
+            MDRenewWindow 10%
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['renew-window'] == '10%'
+        
+    # test case: ca challenge type - http-01
+    def test_md_310_114(self, env):
+        MDConf(env, text="""
+            MDCAChallenges http-01
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['ca']['challenges'] == ['http-01']
+
+    # test case: ca challenge type - http-01
+    def test_md_310_115(self, env):
+        MDConf(env, text="""
+            MDCAChallenges tls-alpn-01
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['ca']['challenges'] == ['tls-alpn-01']
+
+    # test case: ca challenge type - all
+    def test_md_310_116(self, env):
+        MDConf(env, text="""
+            MDCAChallenges http-01 tls-alpn-01
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['ca']['challenges'] == ['http-01', 'tls-alpn-01']
+
+    # test case: automatically collect md names from vhost config
+    def test_md_310_117(self, env):
+        conf = MDConf(env, text="""
+            MDMember auto
+            MDomain testdomain.org
+            """)
+        conf.add_vhost(port=12346, domains=[
+            "testdomain.org", "test.testdomain.org", "mail.testdomain.org",
+        ])
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['domains'] == \
+               ['testdomain.org', 'test.testdomain.org', 'mail.testdomain.org']
+
+    # add renew window to existing md
+    def test_md_310_118(self, env):
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        MDConf(env, text="""
+            MDRenewWindow 14d
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        stat = env.get_md_status("testdomain.org")
+        assert stat['renew-window'] == '14d'
+
+    # test case: set RSA key length 2048
+    def test_md_310_119(self, env):
+        MDConf(env, text="""
+            MDPrivateKeys RSA 2048
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['privkey'] == {
+            "type": "RSA",
+            "bits": 2048
+        }
+
+    # test case: set RSA key length 4096
+    def test_md_310_120(self, env):
+        MDConf(env, text="""
+            MDPrivateKeys RSA 4096
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['privkey'] == {
+            "type": "RSA",
+            "bits": 4096
+        }
+
+    # test case: require HTTPS
+    def test_md_310_121(self, env):
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            MDRequireHttps temporary
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['require-https'] == "temporary"
+
+    # test case: require OCSP stapling
+    def test_md_310_122(self, env):
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            MDMustStaple on
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['must-staple'] is True
+
+    # test case: remove managed domain from config
+    def test_md_310_200(self, env):
+        dns_list = ["testdomain.org", "www.testdomain.org", "mail.testdomain.org"]
+        env.a2md(["add"] + dns_list)
+        env.check_md(dns_list, state=1)
+        conf = MDConf(env,)
+        conf.install()
+        assert env.apache_restart() == 0
+        # check: md stays in store
+        env.check_md(dns_list, state=1)
+
+    # test case: remove alias DNS from managed domain
+    def test_md_310_201(self, env):
+        dns_list = ["testdomain.org", "test.testdomain.org", "www.testdomain.org", "mail.testdomain.org"]
+        env.a2md(["add"] + dns_list)
+        env.check_md(dns_list, state=1)
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        # check: DNS has been removed from md in store
+        env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"], state=1)
+
+    # test case: remove primary name from managed domain
+    def test_md_310_202(self, env):
+        dns_list = ["name.testdomain.org", "testdomain.org", "www.testdomain.org", "mail.testdomain.org"]
+        env.a2md(["add"] + dns_list)
+        env.check_md(dns_list, state=1)
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        # check: md overwrite previous name and changes name
+        env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"],
+                     md="testdomain.org", state=1)
+
+    # test case: remove one md, keep another
+    def test_md_310_203(self, env):
+        dns_list1 = ["greenbytes2.de", "www.greenbytes2.de", "mail.greenbytes2.de"]
+        dns_list2 = ["testdomain.org", "www.testdomain.org", "mail.testdomain.org"]
+        env.a2md(["add"] + dns_list1)
+        env.a2md(["add"] + dns_list2)
+        env.check_md(dns_list1, state=1)
+        env.check_md(dns_list2, state=1)
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        # all mds stay in store
+        env.check_md(dns_list1, state=1)
+        env.check_md(dns_list2, state=1)
+
+    # test case: remove ca info from md, should switch over to new defaults
+    def test_md_310_204(self, env):
+        name = "testdomain.org"
+        MDConf(env, local_ca=False, text="""
+            MDCertificateAuthority http://acme.test.org:4000/directory
+            MDCertificateProtocol ACME
+            MDCertificateAgreement http://acme.test.org:4000/terms/v1
+
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        # setup: sync with ca info removed
+        MDConf(env, local_ca=False, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
+                     ca="https://acme-v02.api.letsencrypt.org/directory", protocol="ACME")
+
+    # test case: remove server admin from md
+    def test_md_310_205(self, env):
+        name = "testdomain.org"
+        MDConf(env, admin="admin@testdomain.org", text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        # setup: sync with admin info removed
+        MDConf(env, admin="", text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        # check: md stays the same with previous admin info
+        env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
+                     contacts=["mailto:admin@testdomain.org"])
+
+    # test case: remove renew window from conf -> fallback to default
+    def test_md_310_206(self, env):
+        MDConf(env, text="""
+            MDRenewWindow 14d
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['renew-window'] == '14d'
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        # check: renew window not set
+        assert env.a2md(["list"]).json['output'][0]['renew-window'] == '33%'
+
+    # test case: remove drive mode from conf -> fallback to default (auto)
+    @pytest.mark.parametrize("renew_mode,exp_code", [
+        ("manual", 0), 
+        ("auto", 1), 
+        ("always", 2)
+    ])
+    def test_md_310_207(self, env, renew_mode, exp_code):
+        MDConf(env, text="""
+            MDRenewMode %s
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """ % renew_mode).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['renew-mode'] == exp_code
+        #
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['renew-mode'] == 1
+
+    # test case: remove challenges from conf -> fallback to default (not set)
+    def test_md_310_208(self, env):
+        MDConf(env, text="""
+            MDCAChallenges http-01
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['ca']['challenges'] == ['http-01']
+        #
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert 'challenges' not in env.a2md(["list"]).json['output'][0]['ca']
+
+    # test case: specify RSA key
+    @pytest.mark.parametrize("key_size", ["2048", "4096"])
+    def test_md_310_209(self, env, key_size):
+        MDConf(env, text="""
+            MDPrivateKeys RSA %s
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """ % key_size).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['privkey']['type'] == "RSA"
+        #
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert "privkey" not in env.a2md(["list"]).json['output'][0]
+
+    # test case: require HTTPS
+    @pytest.mark.parametrize("mode", ["temporary", "permanent"])
+    def test_md_310_210(self, env, mode):
+        MDConf(env, text="""
+            <MDomainSet testdomain.org>
+                MDMember www.testdomain.org mail.testdomain.org
+                MDRequireHttps %s
+            </MDomainSet>
+            """ % mode).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['require-https'] == mode, \
+            "Unexpected HTTPS require mode in store. config: {}".format(mode)
+        #
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert "require-https" not in env.a2md(["list"]).json['output'][0], \
+            "HTTPS require still persisted in store. config: {}".format(mode)
+
+    # test case: require OCSP stapling
+    def test_md_310_211(self, env):
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            MDMustStaple on
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['must-staple'] is True
+        #
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['must-staple'] is False
+
+    # test case: reorder DNS names in md definition
+    def test_md_310_300(self, env):
+        dns_list = ["testdomain.org", "mail.testdomain.org", "www.testdomain.org"]
+        env.a2md(["add"] + dns_list)
+        env.check_md(dns_list, state=1)
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        # check: dns list changes
+        env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"], state=1)
+
+    # test case: move DNS from one md to another
+    def test_md_310_301(self, env):
+        env.a2md(["add", "testdomain.org", "www.testdomain.org", "mail.testdomain.org", "mail.testdomain2.org"])
+        env.a2md(["add", "testdomain2.org", "www.testdomain2.org"])
+        env.check_md(["testdomain.org", "www.testdomain.org",
+                      "mail.testdomain.org", "mail.testdomain2.org"], state=1)
+        env.check_md(["testdomain2.org", "www.testdomain2.org"], state=1)        
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            MDomain testdomain2.org www.testdomain2.org mail.testdomain2.org
+            """).install()
+        assert env.apache_restart() == 0
+        env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"], state=1)
+        env.check_md(["testdomain2.org", "www.testdomain2.org", "mail.testdomain2.org"], state=1)
+
+    # test case: change ca info
+    def test_md_310_302(self, env):
+        name = "testdomain.org"
+        MDConf(env, local_ca=False, text="""
+            MDCertificateAuthority http://acme.test.org:4000/directory
+            MDCertificateProtocol ACME
+            MDCertificateAgreement http://acme.test.org:4000/terms/v1
+
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        # setup: sync with changed ca info
+        MDConf(env, local_ca=False, admin="webmaster@testdomain.org",
+                  text="""
+            MDCertificateAuthority http://somewhere.com:6666/directory
+            MDCertificateProtocol ACME
+            MDCertificateAgreement http://somewhere.com:6666/terms/v1
+
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        # check: md stays the same with previous ca info
+        env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
+                     ca="http://somewhere.com:6666/directory", protocol="ACME",
+                     agreement="http://somewhere.com:6666/terms/v1")
+
+    # test case: change server admin
+    def test_md_310_303(self, env):
+        name = "testdomain.org"
+        MDConf(env, admin="admin@testdomain.org", text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        # setup: sync with changed admin info
+        MDConf(env, local_ca=False, admin="webmaster@testdomain.org", text="""
+            MDCertificateAuthority http://somewhere.com:6666/directory
+            MDCertificateProtocol ACME
+            MDCertificateAgreement http://somewhere.com:6666/terms/v1
+
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        # check: md stays the same with previous admin info
+        env.check_md([name, "www.testdomain.org", "mail.testdomain.org"], state=1,
+                     contacts=["mailto:webmaster@testdomain.org"])
+
+    # test case: change drive mode - manual -> auto -> always
+    def test_md_310_304(self, env):
+        MDConf(env, text="""
+            MDRenewMode manual
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['renew-mode'] == 0
+        # test case: drive mode auto
+        MDConf(env, text="""
+            MDRenewMode auto
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['renew-mode'] == 1
+        # test case: drive mode always
+        MDConf(env, text="""
+            MDRenewMode always
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['renew-mode'] == 2
+
+    # test case: change config value for renew window, use various syntax alternatives
+    def test_md_310_305(self, env):
+        MDConf(env, text="""
+            MDRenewWindow 14d
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        md = env.a2md(["list"]).json['output'][0]
+        assert md['renew-window'] == '14d'
+        MDConf(env, text="""
+            MDRenewWindow 10
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        md = env.a2md(["list"]).json['output'][0]
+        assert md['renew-window'] == '10d'
+        MDConf(env, text="""
+            MDRenewWindow 10%
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        md = env.a2md(["list"]).json['output'][0]
+        assert md['renew-window'] == '10%'
+
+    # test case: change challenge types - http -> tls-sni -> all
+    def test_md_310_306(self, env):
+        MDConf(env, text="""
+            MDCAChallenges http-01
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['ca']['challenges'] == ['http-01']
+        # test case: drive mode auto
+        MDConf(env, text="""
+            MDCAChallenges tls-alpn-01
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['ca']['challenges'] == ['tls-alpn-01']
+        # test case: drive mode always
+        MDConf(env, text="""
+            MDCAChallenges http-01 tls-alpn-01
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['ca']['challenges'] == ['http-01', 'tls-alpn-01']
+
+    # test case:  RSA key length: 4096 -> 2048 -> 4096
+    def test_md_310_307(self, env):
+        MDConf(env, text="""
+            MDPrivateKeys RSA 4096
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['privkey'] == {
+            "type": "RSA",
+            "bits": 4096
+        }
+        MDConf(env, text="""
+            MDPrivateKeys RSA 2048
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['privkey'] == {
+            "type": "RSA",
+            "bits": 2048
+        }
+        MDConf(env, text="""
+            MDPrivateKeys RSA 4096
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['privkey'] == {
+            "type": "RSA",
+            "bits": 4096
+        }
+
+    # test case: change HTTPS require settings on existing md
+    def test_md_310_308(self, env):
+        # setup: nothing set
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert "require-https" not in env.a2md(["list"]).json['output'][0]
+        # test case: temporary redirect
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            MDRequireHttps temporary
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['require-https'] == "temporary"
+        # test case: permanent redirect
+        MDConf(env, text="""
+            <MDomainSet testdomain.org>
+                MDMember www.testdomain.org mail.testdomain.org
+                MDRequireHttps permanent
+            </MDomainSet>
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['require-https'] == "permanent"
+
+    # test case: change OCSP stapling settings on existing md
+    def test_md_310_309(self, env):
+        # setup: nothing set
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['must-staple'] is False
+        # test case: OCSP stapling on
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            MDMustStaple on
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['must-staple'] is True
+        # test case: OCSP stapling off
+        MDConf(env, text="""
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            MDMustStaple off
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'][0]['must-staple'] is False
+
+    # test case: change renew window parameter
+    @pytest.mark.parametrize("window", [
+        "0%", "33d", "40%"
+    ])
+    def test_md_310_310(self, env, window):
+        # non-default renewal setting
+        domain = self.test_domain
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.start_md([domain])
+        conf.add_drive_mode("manual")
+        conf.add_renew_window(window)
+        conf.end_md()
+        conf.add_vhost(domains=domain)
+        conf.install()
+        assert env.apache_restart() == 0
+        stat = env.get_md_status(domain)
+        assert stat["renew-window"] == window
+
+    # test case: add dns name on existing valid md
+    def test_md_310_400(self, env):
+        # setup: create complete md in store
+        domain = self.test_domain
+        name = "www." + domain
+        assert env.a2md(["add", name, "test1." + domain]).exit_code == 0
+        assert env.a2md(["update", name, "contacts", "admin@" + name]).exit_code == 0
+        assert env.a2md(["update", name, "agreement", env.acme_tos]).exit_code == 0
+        MDConf(env).install()
+        assert env.apache_restart() == 0
+
+        # setup: drive it
+        r = env.a2md(["-v", "drive", name])
+        assert r.exit_code == 0, "drive not successfull: {0}".format(r.stderr)
+        assert env.a2md(["list", name]).json['output'][0]['state'] == env.MD_S_COMPLETE
+
+        # remove one domain -> status stays COMPLETE
+        assert env.a2md(["update", name, "domains", name]).exit_code == 0
+        assert env.a2md(["list", name]).json['output'][0]['state'] == env.MD_S_COMPLETE
+        
+        # add other domain -> status INCOMPLETE
+        assert env.a2md(["update", name, "domains", name, "test2." + domain]).exit_code == 0
+        assert env.a2md(["list", name]).json['output'][0]['state'] == env.MD_S_INCOMPLETE
+
+    # test case: change ca info
+    def test_md_310_401(self, env):
+        # setup: create complete md in store
+        domain = self.test_domain
+        name = "www." + domain
+        assert env.a2md(["add", name]).exit_code == 0
+        assert env.a2md(["update", name, "contacts", "admin@" + name]).exit_code == 0
+        assert env.a2md(["update", name, "agreement", env.acme_tos]).exit_code == 0
+        assert env.apache_restart() == 0
+        # setup: drive it
+        assert env.a2md(["drive", name]).exit_code == 0
+        assert env.a2md(["list", name]).json['output'][0]['state'] == env.MD_S_COMPLETE
+        # setup: change CA URL
+        assert env.a2md(["update", name, "ca", env.acme_url]).exit_code == 0
+        # check: state stays COMPLETE
+        assert env.a2md(["list", name]).json['output'][0]['state'] == env.MD_S_COMPLETE
+
+    # test case: change the store dir
+    def test_md_310_500(self, env):
+        MDConf(env, text="""
+            MDStoreDir md-other
+            MDomain testdomain.org www.testdomain.org mail.testdomain.org
+            """).install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list"]).json['output'] == []
+        env.set_store_dir("md-other")
+        env.check_md(["testdomain.org", "www.testdomain.org", "mail.testdomain.org"], state=1)
+        env.clear_store()
+        env.set_store_dir_default()
+
+    # test case: place an unexpected file into the store, check startup survival, see #218
+    def test_md_310_501(self, env):
+        # setup: create complete md in store
+        domain = self.test_domain
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.start_md([domain])
+        conf.end_md()
+        conf.add_vhost(domains=[domain])
+        conf.install()
+        assert env.apache_restart() == 0
+        # add a file at top level
+        assert env.await_completion([domain])
+        fpath = os.path.join(env.store_domains(), "wrong.com")
+        with open(fpath, 'w') as fd:
+            fd.write("this does not belong here\n")
+        assert env.apache_restart() == 0
+
+    # test case: add external account binding
+    def test_md_310_601(self, env):
+        domain = self.test_domain
+        # directly set
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.start_md([domain])
+        conf.add_drive_mode("manual")
+        conf.add("MDExternalAccountBinding k123 hash123")
+        conf.end_md()
+        conf.add_vhost(domains=domain)
+        conf.install()
+        assert env.apache_restart() == 0
+        stat = env.get_md_status(domain)
+        assert stat["eab"] == {'kid': 'k123', 'hmac': '***'}
+        # eab inherited
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add("MDExternalAccountBinding k456 hash456")
+        conf.start_md([domain])
+        conf.add_drive_mode("manual")
+        conf.end_md()
+        conf.add_vhost(domains=domain)
+        conf.install()
+        assert env.apache_restart() == 0
+        stat = env.get_md_status(domain)
+        assert stat["eab"] == {'kid': 'k456', 'hmac': '***'}
+        # override eab inherited
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add("MDExternalAccountBinding k456 hash456")
+        conf.start_md([domain])
+        conf.add_drive_mode("manual")
+        conf.add("MDExternalAccountBinding none")
+        conf.end_md()
+        conf.add_vhost(domains=domain)
+        conf.install()
+        assert env.apache_restart() == 0
+        stat = env.get_md_status(domain)
+        assert "eab" not in stat
+

Added: httpd/httpd/trunk/test/modules/md/test_502_acmev2_drive.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_502_acmev2_drive.py?rev=1894611&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_502_acmev2_drive.py (added)
+++ httpd/httpd/trunk/test/modules/md/test_502_acmev2_drive.py Fri Oct 29 10:05:29 2021
@@ -0,0 +1,549 @@
+# test driving the ACMEv2 protocol
+
+import base64
+import json
+import os.path
+import re
+import time
+
+import pytest
+
+from .md_conf import MDConf, MDConf
+from .md_cert_util import MDCertUtil
+from .md_env import MDTestEnv
+
+
+@pytest.mark.skipif(condition=not MDTestEnv.has_a2md(), reason="no a2md available")
+@pytest.mark.skipif(condition=not MDTestEnv.has_acme_server(),
+                    reason="no ACME test server configured")
+class TestDrivev2:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env, acme):
+        acme.start(config='default')
+        env.check_acme()
+        env.APACHE_CONF_SRC = "data/test_drive"
+        MDConf(env).install()
+        assert env.apache_restart() == 0
+
+    @pytest.fixture(autouse=True, scope='function')
+    def _method_scope(self, env, request):
+        env.clear_store()
+        MDConf(env).install()
+        self.test_domain = env.get_request_domain(request)
+
+    # --------- invalid precondition ---------
+
+    def test_md_502_000(self, env):
+        # test case: md without contact info
+        domain = self.test_domain
+        name = "www." + domain
+        assert env.a2md(["add", name]).exit_code == 0
+        r = env.a2md(["drive", name])
+        assert r.exit_code == 1
+        assert re.search("No contact information", r.stderr)
+
+    def test_md_502_001(self, env):
+        # test case: md with contact, but without TOS
+        domain = self.test_domain
+        name = "www." + domain
+        assert env.a2md(["add", name]).exit_code == 0
+        assert env.a2md( 
+            ["update", name, "contacts", "admin@test1.not-forbidden.org"]
+            ).exit_code == 0
+        r = env.a2md(["drive", name])
+        assert r.exit_code == 1
+        assert re.search("the CA requires you to accept the terms-of-service as specified in ", r.stderr)
+
+    # test_102 removed, was based on false assumption
+    def test_md_502_003(self, env):
+        # test case: md with unknown protocol FOO
+        domain = self.test_domain
+        name = "www." + domain
+        self._prepare_md(env, [name])
+        assert env.a2md(
+            ["update", name, "ca", env.acme_url, "FOO"]
+            ).exit_code == 0
+        r = env.a2md(["drive", name])
+        assert r.exit_code == 1
+        assert re.search("Unknown CA protocol", r.stderr)
+
+    # --------- driving OK ---------
+
+    def test_md_502_100(self, env):
+        # test case: md with one domain
+        domain = self.test_domain
+        name = "www." + domain
+        self._prepare_md(env, [name])
+        assert env.apache_restart() == 0
+        # drive
+        prev_md = env.a2md(["list", name]).json['output'][0]
+        r = env.a2md(["-vv", "drive", "-c", "http-01", name])
+        assert r.exit_code == 0, "a2md drive failed: {0}".format(r.stderr)
+        env.check_md_credentials([name])
+        self._check_account_key(env, name)
+
+        # check archive content
+        store_md = json.loads(open(env.store_archived_file(name, 1, 'md.json')).read())
+        for f in ['name', 'ca', 'domains', 'contacts', 'renew-mode', 'renew-window', 'must-staple']:
+            assert store_md[f] == prev_md[f]
+        
+        # check file system permissions:
+        env.check_file_permissions(name)
+        # check: challenges removed
+        env.check_dir_empty(env.store_challenges())
+        # check how the challenge resources are answered in sevceral combinations 
+        r = env.get_meta(domain, "/.well-known/acme-challenge", False)
+        assert r.exit_code == 0
+        assert r.response['status'] == 404
+        r = env.get_meta(domain, "/.well-known/acme-challenge/", False)
+        assert r.exit_code == 0
+        assert r.response['status'] == 404
+        r = env.get_meta(domain, "/.well-known/acme-challenge/123", False)
+        assert r.exit_code == 0
+        assert r.response['status'] == 404
+        assert r.exit_code == 0
+        cdir = os.path.join(env.store_challenges(), domain)
+        os.makedirs(cdir)
+        open(os.path.join(cdir, 'acme-http-01.txt'), "w").write("content-of-123")
+        r = env.get_meta(domain, "/.well-known/acme-challenge/123", False)
+        assert r.exit_code == 0
+        assert r.response['status'] == 200
+        assert r.response['header']['content-length'] == '14'
+
+    def test_md_502_101(self, env):
+        # test case: md with 2 domains
+        domain = self.test_domain
+        name = "www." + domain
+        self._prepare_md(env, [name, "test." + domain])
+        assert env.apache_restart() == 0
+        # drive
+        r = env.a2md(["-vv", "drive", "-c", "http-01", name])
+        assert r.exit_code == 0, "a2md drive failed: {0}".format(r.stderr)
+        env.check_md_credentials([name, "test." + domain])
+
+    # test_502_102 removed, as accounts without ToS are not allowed in ACMEv2
+
+    def test_md_502_103(self, env):
+        # test case: md with one domain, ACME account and TOS agreement on server
+        # setup: create md
+        domain = self.test_domain
+        name = "www." + domain
+        assert env.a2md(["add", name]).exit_code == 0
+        assert env.a2md(["update", name, "contacts", "admin@" + domain]).exit_code == 0
+        assert env.apache_restart() == 0
+        # setup: create account on server
+        r = env.a2md(["-t", "accepted", "acme", "newreg", "admin@" + domain], raw=True)
+        assert r.exit_code == 0
+        acct = re.match("registered: (.*)$", r.stdout).group(1)
+        # setup: link md to account
+        assert env.a2md(["update", name, "account", acct]).exit_code == 0
+        # drive
+        r = env.a2md(["-vv", "drive", name])
+        assert r.exit_code == 0, "a2md drive failed: {0}".format(r.stderr)
+        env.check_md_credentials([name])
+
+    # test_502_104 removed, order are created differently in ACMEv2
+
+    def test_md_502_105(self, env):
+        # test case: md with one domain, local TOS agreement and ACME account that is deleted (!) on server
+        # setup: create md
+        domain = self.test_domain
+        name = "www." + domain
+        self._prepare_md(env, [name])
+        assert env.apache_restart() == 0
+        # setup: create account on server
+        r = env.a2md(["-t", "accepted", "acme", "newreg", "test@" + domain], raw=True)
+        assert r.exit_code == 0
+        acct = re.match("registered: (.*)$", r.stdout).group(1)
+        # setup: link md to account
+        assert env.a2md(["update", name, "account", acct]).exit_code == 0
+        # setup: delete account on server
+        assert env.a2md(["acme", "delreg", acct]).exit_code == 0
+        # drive
+        r = env.a2md(["drive", name])
+        assert r.exit_code == 0, "a2md drive failed: {0}".format(r.stderr)
+        env.check_md_credentials([name])
+
+    def test_md_502_107(self, env):
+        # test case: drive again on COMPLETE md, then drive --force
+        # setup: prepare md in store
+        domain = self.test_domain
+        name = "www." + domain
+        self._prepare_md(env, [name])
+        assert env.apache_restart() == 0
+        # drive
+        r = env.a2md(["-vv", "drive", name])
+        assert r.exit_code == 0, "a2md drive failed: {0}".format(r.stderr)
+        env.check_md_credentials([name])
+        orig_cert = MDCertUtil(env.store_domain_file(name, 'pubcert.pem'))
+
+        # drive again
+        assert env.a2md(["-vv", "drive", name]).exit_code == 0
+        env.check_md_credentials([name])
+        cert = MDCertUtil(env.store_domain_file(name, 'pubcert.pem'))
+        # check: cert not changed
+        assert cert.same_serial_as(orig_cert)
+
+        # drive --force
+        assert env.a2md(["-vv", "drive", "--force", name]).exit_code == 0
+        env.check_md_credentials([name])
+        cert = MDCertUtil(env.store_domain_file(name, 'pubcert.pem'))
+        # check: cert not changed
+        assert not cert.same_serial_as(orig_cert)
+        # check: previous cert was archived
+        cert = MDCertUtil(env.store_archived_file(name, 2, 'pubcert.pem'))
+        assert cert.same_serial_as(orig_cert)
+
+    def test_md_502_108(self, env):
+        # test case: drive via HTTP proxy
+        domain = self.test_domain
+        name = "www." + domain
+        self._prepare_md(env, [name])
+        conf = MDConf(env, proxy=True)
+        conf.add('LogLevel proxy:trace8')
+        conf.install()
+        assert env.apache_restart() == 0
+
+        # drive it, with wrong proxy url -> FAIL
+        r = env.a2md(["-p", "http://localhost:1", "drive", name])
+        assert r.exit_code == 1
+        assert "Connection refused" in r.stderr
+
+        # drive it, working proxy url -> SUCCESS
+        proxy_url = f"http://localhost:{env.proxy_port}"
+        r = env.a2md(["-vv", "-p", proxy_url, "drive", name])
+        assert 0 == r.exit_code, "a2md failed: {0}".format(r.stderr)
+        env.check_md_credentials([name])
+
+    def test_md_502_109(self, env):
+        # test case: redirect on SSL-only domain
+        # setup: prepare config
+        domain = self.test_domain
+        name = "www." + domain
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_drive_mode("manual")
+        conf.add_md([name])
+        conf.add_vhost(name, port=env.http_port, doc_root="htdocs/test")
+        conf.add_vhost(name, doc_root="htdocs/test")
+        conf.install()
+        # setup: create resource files
+        self._write_res_file(os.path.join(env.server_docs_dir, "test"), "name.txt", name)
+        self._write_res_file(os.path.join(env.server_docs_dir), "name.txt", "not-forbidden.org")
+        assert env.apache_restart() == 0
+
+        # drive it
+        assert env.a2md(["drive", name]).exit_code == 0
+        assert env.apache_restart() == 0
+        # test HTTP access - no redirect
+        jdata = env.get_json_content(f"test1.{env.http_tld}", "/alive.json", use_https=False)
+        assert jdata['host']== "test1"
+        assert env.get_content(name, "/name.txt", use_https=False) == name
+        r = env.get_meta(name, "/name.txt", use_https=False)
+        assert int(r.response['header']['content-length']) == len(name)
+        assert "Location" not in r.response['header']
+        # test HTTPS access
+        assert env.get_content(name, "/name.txt", use_https=True) == name
+
+        # test HTTP access again -> redirect to default HTTPS port
+        conf.add("MDRequireHttps temporary")
+        conf.install()
+        assert env.apache_restart() == 0
+        r = env.get_meta(name, "/name.txt", use_https=False)
+        assert r.response['status'] == 302
+        exp_location = "https://%s/name.txt" % name
+        assert r.response['header']['location'] == exp_location
+        # should not see this
+        assert 'strict-transport-security' not in r.response['header']
+        # test default HTTP vhost -> still no redirect
+        jdata = env.get_json_content(f"test1.{env.http_tld}", "/alive.json", use_https=False)
+        assert jdata['host']== "test1"
+        r = env.get_meta(name, "/name.txt", use_https=True)
+        # also not for this
+        assert 'strict-transport-security' not in r.response['header']
+
+        # test HTTP access again -> redirect permanent
+        conf.add("MDRequireHttps permanent")
+        conf.install()
+        assert env.apache_restart() == 0
+        r = env.get_meta(name, "/name.txt", use_https=False)
+        assert r.response['status'] == 301
+        exp_location = "https://%s/name.txt" % name
+        assert r.response['header']['location'] == exp_location
+        assert 'strict-transport-security' not in r.response['header']
+        # should see this
+        r = env.get_meta(name, "/name.txt", use_https=True)
+        assert r.response['header']['strict-transport-security'] == 'max-age=15768000'
+
+    def test_md_502_110(self, env):
+        # test case: SSL-only domain, override headers generated by mod_md 
+        # setup: prepare config
+        domain = self.test_domain
+        name = "www." + domain
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_drive_mode("manual")
+        conf.add("MDRequireHttps permanent")
+        conf.add_md([name])
+        conf.add_vhost(name, port=env.http_port)
+        conf.add_vhost(name)
+        conf.install()
+        assert env.apache_restart() == 0
+        # drive it
+        assert env.a2md(["drive", name]).exit_code == 0
+        assert env.apache_restart() == 0
+
+        # test override HSTS header
+        conf.add('Header set Strict-Transport-Security "max-age=10886400; includeSubDomains; preload"')
+        conf.install()
+        assert env.apache_restart() == 0
+        r = env.get_meta(name, "/name.txt", use_https=True)
+        assert 'strict-transport-security' in r.response['header'], r.response['header']
+        assert r.response['header']['strict-transport-security'] == \
+               'max-age=10886400; includeSubDomains; preload'
+
+        # test override Location header
+        conf.add('  Redirect /a /name.txt')
+        conf.add('  Redirect seeother /b /name.txt')
+        conf.install()
+        assert env.apache_restart() == 0
+        # check: default redirect by mod_md still works
+        exp_location = "https://%s/name.txt" % name
+        r = env.get_meta(name, "/name.txt", use_https=False)
+        assert r.response['status'] == 301
+        assert r.response['header']['location'] == exp_location
+        # check: redirect as given by mod_alias
+        exp_location = "https://%s/a" % name
+        r = env.get_meta(name, "/a", use_https=False)
+        assert r.response['status'] == 301    # FAIL: mod_alias generates Location header instead of mod_md
+        assert r.response['header']['location'] == exp_location
+
+    def test_md_502_111(self, env):
+        # test case: vhost with parallel HTTP/HTTPS, check mod_alias redirects
+        # setup: prepare config
+        domain = self.test_domain
+        name = "www." + domain
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_drive_mode("manual")
+        conf.add_md([name])
+        conf.add("  LogLevel alias:debug")
+        conf.add_vhost(name, port=env.http_port)
+        conf.add_vhost(name)
+        conf.install()
+        assert env.apache_restart() == 0
+        # drive it
+        r = env.a2md(["-v", "drive", name])
+        assert r.exit_code == 0, "a2md drive failed: {0}".format(r.stderr)
+        assert env.apache_restart() == 0
+
+        # setup: place redirect rules
+        conf.add('  Redirect /a /name.txt')
+        conf.add('  Redirect seeother /b /name.txt')
+        conf.install()
+        assert env.apache_restart() == 0
+        # check: redirects on HTTP
+        exp_location = "http://%s:%s/name.txt" % (name, env.http_port)
+        r = env.get_meta(name, "/a", use_https=False)
+        assert r.response['status'] == 302
+        assert r.response['header']['location'] == exp_location
+        r = env.get_meta(name, "/b", use_https=False)
+        assert r.response['status'] == 303
+        assert r.response['header']['location'] == exp_location
+        # check: redirects on HTTPS
+        exp_location = "https://%s:%s/name.txt" % (name, env.https_port)
+        r = env.get_meta(name, "/a", use_https=True)
+        assert r.response['status'] == 302
+        assert r.response['header']['location'] == exp_location     # FAIL: expected 'https://...' but found 'http://...'
+        r = env.get_meta(name, "/b", use_https=True)
+        assert r.response['status'] == 303
+        assert r.response['header']['location'] == exp_location
+
+    def test_md_502_120(self, env):
+        # test case: NP dereference reported by Daniel Caminada <da...@ergon.ch>
+        domain = self.test_domain
+        name = "www." + domain
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_drive_mode("manual")
+        conf.add_md([name])
+        conf.add_vhost(name)
+        conf.install()
+        assert env.apache_restart() == 0
+        env.run(["openssl", "s_client",
+                 f"-connect", "localhost:{env.https_port}",
+                 "-servername", "example.com", "-crlf"
+                 ], input="GET https:// HTTP/1.1\nHost: example.com\n\n")
+        assert env.apache_restart() == 0
+
+    # --------- critical state change -> drive again ---------
+
+    def test_md_502_200(self, env):
+        # test case: add dns name on existing valid md
+        # setup: create md in store
+        domain = self.test_domain
+        name = "www." + domain
+        self._prepare_md(env, [name])
+        assert env.apache_restart() == 0
+        # setup: drive it
+        r = env.a2md(["drive", name])
+        assert r.exit_code == 0, "a2md drive failed: {0}".format(r.stderr)
+        old_cert = MDCertUtil(env.store_domain_file(name, 'pubcert.pem'))
+        # setup: add second domain
+        assert env.a2md(["update", name, "domains", name, "test." + domain]).exit_code == 0
+        # drive
+        r = env.a2md(["-vv", "drive", name])
+        assert r.exit_code == 0, "a2md drive failed: {0}".format(r.stderr)
+        # check new cert
+        env.check_md_credentials([name, "test." + domain])
+        new_cert = MDCertUtil(env.store_domain_file(name, 'pubcert.pem'))
+        assert not old_cert.same_serial_as(new_cert.get_serial)
+
+    @pytest.mark.parametrize("renew_window,test_data_list", [
+        ("14d", [
+            {"valid": {"notBefore": -5,   "notAfter": 180}, "renew": False},
+            {"valid": {"notBefore": -200, "notAfter": 15}, "renew": False},
+            {"valid": {"notBefore": -200, "notAfter": 13}, "renew": True},
+        ]),
+        ("30%", [
+            {"valid": {"notBefore": -0,   "notAfter": 180}, "renew": False},
+            {"valid": {"notBefore": -120, "notAfter": 60}, "renew": False},
+            {"valid": {"notBefore": -126, "notAfter": 53}, "renew": True},
+        ])
+    ])
+    def test_md_502_201(self, env, renew_window, test_data_list):
+        # test case: trigger cert renew when entering renew window 
+        # setup: prepare COMPLETE md
+        domain = self.test_domain
+        name = "www." + domain
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_drive_mode("manual")
+        conf.add_renew_window(renew_window)
+        conf.add_md([name])
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list", name]).json['output'][0]['state'] == env.MD_S_INCOMPLETE
+        # setup: drive it
+        r = env.a2md(["drive", name])
+        assert r.exit_code == 0, "a2md drive failed: {0}".format(r.stderr)
+        cert1 = MDCertUtil(env.store_domain_file(name, 'pubcert.pem'))
+        assert env.a2md(["list", name]).json['output'][0]['state'] == env.MD_S_COMPLETE
+
+        # replace cert by self-signed one -> check md status
+        print("TRACE: start testing renew window: %s" % renew_window)
+        for tc in test_data_list:
+            print("TRACE: create self-signed cert: %s" % tc["valid"])
+            env.create_self_signed_cert([name], tc["valid"])
+            cert2 = MDCertUtil(env.store_domain_file(name, 'pubcert.pem'))
+            assert not cert2.same_serial_as(cert1)
+            md = env.a2md(["list", name]).json['output'][0]
+            assert md["renew"] == tc["renew"], \
+                "Expected renew == {} indicator in {}, test case {}".format(tc["renew"], md, tc)
+
+    @pytest.mark.parametrize("key_type,key_params,exp_key_length", [
+        ("RSA", [2048], 2048),
+        ("RSA", [3072], 3072),
+        ("RSA", [4096], 4096),
+        ("Default", [], 2048)
+    ])
+    def test_md_502_202(self, env, key_type, key_params, exp_key_length):
+        # test case: specify RSA key length and verify resulting cert key 
+        # setup: prepare md
+        domain = self.test_domain
+        name = "www." + domain
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_drive_mode("manual")
+        conf.add_private_key(key_type, key_params)
+        conf.add_md([name])
+        conf.install()
+        assert env.apache_restart() == 0
+        assert env.a2md(["list", name]).json['output'][0]['state'] == env.MD_S_INCOMPLETE
+        # setup: drive it
+        r = env.a2md(["-vv", "drive", name])
+        assert r.exit_code == 0, "drive for MDPrivateKeys {} {}: {}".format(key_type, key_params, r.stderr)
+        assert env.a2md(["list", name]).json['output'][0]['state'] == env.MD_S_COMPLETE
+        # check cert key length
+        cert = MDCertUtil(env.store_domain_file(name, 'pubcert.pem'))
+        assert cert.get_key_length() == exp_key_length
+
+    # test_502_203 removed, as ToS agreement is not really checked in ACMEv2
+
+    # --------- non-critical state change -> keep data ---------
+
+    def test_md_502_300(self, env):
+        # test case: remove one domain name from existing valid md
+        # setup: create md in store
+        domain = self.test_domain
+        name = "www." + domain
+        self._prepare_md(env, [name, "test." + domain, "xxx." + domain])
+        assert env.apache_restart() == 0
+        # setup: drive it
+        r = env.a2md(["drive", name])
+        assert r.exit_code == 0, "a2md drive failed: {0}".format(r.stderr)
+        old_cert = MDCertUtil(env.store_domain_file(name, 'pubcert.pem'))
+        # setup: remove one domain
+        assert env.a2md(["update", name, "domains"] + [name, "test." + domain]).exit_code == 0
+        # drive
+        assert env.a2md(["-vv", "drive", name]).exit_code == 0
+        # compare cert serial
+        new_cert = MDCertUtil(env.store_domain_file(name, 'pubcert.pem'))
+        assert old_cert.same_serial_as(new_cert)
+
+    def test_md_502_301(self, env):
+        # test case: change contact info on existing valid md
+        # setup: create md in store
+        domain = self.test_domain
+        name = "www." + domain
+        self._prepare_md(env, [name])
+        assert env.apache_restart() == 0
+        # setup: drive it
+        r = env.a2md(["drive", name])
+        assert r.exit_code == 0, "a2md drive failed: {0}".format(r.stderr)
+        old_cert = MDCertUtil(env.store_domain_file(name, 'pubcert.pem'))
+        # setup: add second domain
+        assert env.a2md(["update", name, "contacts", "test@" + domain]).exit_code == 0
+        # drive
+        assert env.a2md(["drive", name]).exit_code == 0
+        # compare cert serial
+        new_cert = MDCertUtil(env.store_domain_file(name, 'pubcert.pem'))
+        assert old_cert.same_serial_as(new_cert)
+
+    # --------- network problems ---------
+
+    def test_md_502_400(self, env):
+        # test case: server not reachable
+        domain = self.test_domain
+        name = "www." + domain
+        self._prepare_md(env, [name])
+        assert env.a2md(
+            ["update", name, "ca", "http://localhost:4711/directory"]
+            ).exit_code == 0
+        # drive
+        r = env.a2md(["drive", name])
+        assert r.exit_code == 1
+        assert r.json['status'] != 0
+        assert r.json['description'] == 'Connection refused'
+
+    # --------- _utils_ ---------
+
+    def _prepare_md(self, env, domains):
+        assert env.a2md(["add"] + domains).exit_code == 0
+        assert env.a2md(
+            ["update", domains[0], "contacts", "admin@" + domains[0]]
+            ).exit_code == 0
+        assert env.a2md( 
+            ["update", domains[0], "agreement", env.acme_tos]
+            ).exit_code == 0
+
+    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)
+
+    RE_MSG_OPENSSL_BAD_DECRYPT = re.compile('.*\'bad decrypt\'.*')
+
+    def _check_account_key(self, env, name):
+        # read encryption key
+        md_store = json.loads(open(env.path_store_json(), 'r').read())
+        encrypt_key = base64.urlsafe_b64decode(str(md_store['key']))
+        # check: key file is encrypted PEM
+        md = env.a2md(["list", name]).json['output'][0]
+        acc = md['ca']['account']
+        MDCertUtil.validate_privkey(env.path_account_key(acc), lambda *args: encrypt_key)

Added: httpd/httpd/trunk/test/modules/md/test_602_roundtrip.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_602_roundtrip.py?rev=1894611&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_602_roundtrip.py (added)
+++ httpd/httpd/trunk/test/modules/md/test_602_roundtrip.py Fri Oct 29 10:05:29 2021
@@ -0,0 +1,143 @@
+# test mod_md basic configurations
+
+import os
+
+import pytest
+
+from .md_conf import MDConf
+from .md_env import MDTestEnv
+
+
+@pytest.mark.skipif(condition=not MDTestEnv.has_a2md(), reason="no a2md available")
+@pytest.mark.skipif(condition=not MDTestEnv.has_acme_server(),
+                    reason="no ACME test server configured")
+class TestRoundtripv2:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env, acme):
+        acme.start(config='default')
+        env.APACHE_CONF_SRC = "data/test_roundtrip"
+        env.clear_store()
+        MDConf(env).install()
+
+    @pytest.fixture(autouse=True, scope='function')
+    def _method_scope(self, env, request):
+        env.check_acme()
+        self.test_domain = env.get_request_domain(request)
+
+    # --------- add to store ---------
+
+    def test_md_602_000(self, env):
+        # test case: generate config with md -> restart -> drive -> generate config
+        # with vhost and ssl -> restart -> check HTTPS access
+        domain = self.test_domain
+        domains = [domain, "www." + domain]
+
+        # - generate config with one md
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_drive_mode("manual")
+        conf.add_md(domains)
+        conf.install()
+        # - restart, check that md is in store
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        # - drive
+        assert env.a2md(["-v", "drive", domain]).exit_code == 0
+        assert env.apache_restart() == 0
+        env.check_md_complete(domain)
+        # - append vhost to config
+        conf.add_vhost(domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        # check: SSL is running OK
+        cert = env.get_cert(domain)
+        assert domain in cert.get_san_list()
+
+        # check file system permissions:
+        env.check_file_permissions(domain)
+
+    def test_md_602_001(self, env):
+        # test case: same as test_600_000, but with two parallel managed domains
+        domain_a = "a-" + self.test_domain
+        domain_b = "b-" + self.test_domain
+        # - generate config with one md
+        domains_a = [domain_a, "www." + domain_a]
+        domains_b = [domain_b, "www." + domain_b]
+
+        conf = MDConf(env)
+        conf.add_drive_mode("manual")
+        conf.add_md(domains_a)
+        conf.add_md(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)
+
+        # - drive
+        assert env.a2md(["drive", domain_a]).exit_code == 0
+        assert env.a2md(["drive", domain_b]).exit_code == 0
+        assert env.apache_restart() == 0
+        env.check_md_complete(domain_a)
+        env.check_md_complete(domain_b)
+
+        # - append vhost to config
+        conf.add_vhost(domains_a)
+        conf.add_vhost(domains_b)
+        conf.install()
+
+        # check: SSL is running OK
+        assert env.apache_restart() == 0
+        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()
+
+    def test_md_602_002(self, env):
+        # test case: one md, that covers two vhosts
+        domain = self.test_domain
+        name_a = "a." + domain
+        name_b = "b." + domain
+        domains = [domain, name_a, name_b]
+
+        # - generate config with one md
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_drive_mode("manual")
+        conf.add_md(domains)
+        conf.install()
+        
+        # - restart, check that md is in store
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+
+        # - drive
+        assert env.a2md(["drive", domain]).exit_code == 0
+        assert env.apache_restart() == 0
+        env.check_md_complete(domain)
+
+        # - append vhost to config
+        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)
+
+        # check: SSL is running OK
+        assert env.apache_restart() == 0
+        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
+
+    # --------- _utils_ ---------
+
+    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)