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/12/14 11:33:27 UTC

svn commit: r1895947 [1/5] - in /httpd/httpd/branches/2.4.x/test/modules/md: ./ data/ data/store_migrate/ data/store_migrate/1.0/ data/store_migrate/1.0/sample1/ data/store_migrate/1.0/sample1/accounts/ data/store_migrate/1.0/sample1/accounts/ACME-loca...

Author: icing
Date: Tue Dec 14 11:33:27 2021
New Revision: 1895947

URL: http://svn.apache.org/viewvc?rev=1895947&view=rev
Log:
  *) test: adding modules/md test suite


Added:
    httpd/httpd/branches/2.4.x/test/modules/md/
    httpd/httpd/branches/2.4.x/test/modules/md/__init__.py
    httpd/httpd/branches/2.4.x/test/modules/md/conftest.py   (with props)
    httpd/httpd/branches/2.4.x/test/modules/md/data/
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/accounts/
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/accounts/ACME-localhost-0000/
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/accounts/ACME-localhost-0000/account.json
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/accounts/ACME-localhost-0000/account.pem
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/archive/
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/archive/7007-1502285564.org.1/
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/archive/7007-1502285564.org.1/md.json
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/challenges/
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/cert.pem
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/chain.pem
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/md.json
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/pkey.pem
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/httpd.json
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/md_store.json
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/staging/
    httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/tmp/
    httpd/httpd/branches/2.4.x/test/modules/md/data/test_920/
    httpd/httpd/branches/2.4.x/test/modules/md/data/test_920/002.pubcert
    httpd/httpd/branches/2.4.x/test/modules/md/data/test_conf_validate/
    httpd/httpd/branches/2.4.x/test/modules/md/data/test_conf_validate/test_014.conf
    httpd/httpd/branches/2.4.x/test/modules/md/data/test_drive/
    httpd/httpd/branches/2.4.x/test/modules/md/data/test_drive/test1.example.org.conf
    httpd/httpd/branches/2.4.x/test/modules/md/data/test_roundtrip/
    httpd/httpd/branches/2.4.x/test/modules/md/data/test_roundtrip/temp.conf
    httpd/httpd/branches/2.4.x/test/modules/md/dns01.py   (with props)
    httpd/httpd/branches/2.4.x/test/modules/md/http_challenge_foobar.py   (with props)
    httpd/httpd/branches/2.4.x/test/modules/md/md_acme.py   (with props)
    httpd/httpd/branches/2.4.x/test/modules/md/md_cert_util.py   (with props)
    httpd/httpd/branches/2.4.x/test/modules/md/md_certs.py   (with props)
    httpd/httpd/branches/2.4.x/test/modules/md/md_conf.py   (with props)
    httpd/httpd/branches/2.4.x/test/modules/md/md_env.py   (with props)
    httpd/httpd/branches/2.4.x/test/modules/md/message.py   (with props)
    httpd/httpd/branches/2.4.x/test/modules/md/msg_fail_on.py   (with props)
    httpd/httpd/branches/2.4.x/test/modules/md/notifail.py   (with props)
    httpd/httpd/branches/2.4.x/test/modules/md/notify.py   (with props)
    httpd/httpd/branches/2.4.x/test/modules/md/pebble/
    httpd/httpd/branches/2.4.x/test/modules/md/pebble/pebble-eab.json.template
    httpd/httpd/branches/2.4.x/test/modules/md/pebble/pebble.json.template
    httpd/httpd/branches/2.4.x/test/modules/md/test_001_store.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_010_store_migrate.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_100_reg_add.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_110_reg_update.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_120_reg_list.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_202_acmev2_regs.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_300_conf_validate.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_310_conf_store.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_502_acmev2_drive.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_602_roundtrip.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_702_auto.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_720_wildcard.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_730_static.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_740_acme_errors.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_741_setup_errors.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_750_eab.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_751_sectigo.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_752_zerossl.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_800_must_staple.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_801_stapling.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_810_ec.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_900_notify.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_901_message.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_910_cleanups.py
    httpd/httpd/branches/2.4.x/test/modules/md/test_920_status.py

Added: httpd/httpd/branches/2.4.x/test/modules/md/__init__.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/__init__.py?rev=1895947&view=auto
==============================================================================
    (empty)

Added: httpd/httpd/branches/2.4.x/test/modules/md/conftest.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/conftest.py?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/conftest.py (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/conftest.py Tue Dec 14 11:33:27 2021
@@ -0,0 +1,89 @@
+import logging
+import os
+import re
+import sys
+import pytest
+
+sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+from .md_conf import HttpdConf
+from .md_env import MDTestEnv
+from .md_acme import MDPebbleRunner, MDBoulderRunner
+
+
+def pytest_report_header(config, startdir):
+    env = MDTestEnv()
+    return "mod_md: [apache: {aversion}({prefix}), mod_{ssl}, ACME server: {acme}]".format(
+        prefix=env.prefix,
+        aversion=env.get_httpd_version(),
+        ssl=env.ssl_module,
+        acme=env.acme_server,
+    )
+
+
+@pytest.fixture(scope="package")
+def env(pytestconfig) -> MDTestEnv:
+    level = logging.INFO
+    console = logging.StreamHandler()
+    console.setLevel(level)
+    console.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
+    logging.getLogger('').addHandler(console)
+    logging.getLogger('').setLevel(level=level)
+    env = MDTestEnv(pytestconfig=pytestconfig)
+    env.setup_httpd()
+    env.apache_access_log_clear()
+    env.httpd_error_log.clear_log()
+    return env
+
+
+@pytest.fixture(autouse=True, scope="package")
+def _session_scope(env):
+    # we'd like to check the httpd error logs after the test suite has
+    # run to catch anything unusual. For this, we setup the ignore list
+    # of errors and warnings that we do expect.
+    env.httpd_error_log.set_ignored_lognos([
+        'AH10040',  # mod_md, setup complain
+        'AH10045',  # mod_md complains that there is no vhost for an MDomain
+        'AH10056',  # mod_md, invalid params
+        'AH10105',  # mod_md does not find a vhost with SSL enabled for an MDomain
+        'AH10085',  # mod_ssl complains about fallback certificates
+        'AH01909',  # mod_ssl, cert alt name complains
+        'AH10170',  # mod_md, wrong config, tested
+        'AH10171',  # mod_md, wrong config, tested
+    ])
+
+    env.httpd_error_log.add_ignored_patterns([
+        re.compile(r'.*urn:ietf:params:acme:error:.*'),
+        re.compile(r'.*None of the ACME challenge methods configured for this domain are suitable.*'),
+        re.compile(r'.*problem\[(challenge-mismatch|challenge-setup-failure|apache:eab-hmac-invalid)].*'),
+        re.compile(r'.*CA considers answer to challenge invalid.].*'),
+        re.compile(r'.*problem\[urn:org:apache:httpd:log:AH\d+:].*'),
+        re.compile(r'.*Unsuccessful in contacting ACME server at :*'),
+        re.compile(r'.*test-md-720-002-\S+.org: dns-01 setup command failed .*'),
+    ])
+    if env.lacks_ocsp():
+        env.httpd_error_log.add_ignored_patterns([
+            re.compile(r'.*certificate with serial \S+ has no OCSP responder URL.*'),
+        ])
+    yield
+    assert env.apache_stop() == 0
+    errors, warnings = env.httpd_error_log.get_missed()
+    assert (len(errors), len(warnings)) == (0, 0),\
+            f"apache logged {len(errors)} errors and {len(warnings)} warnings: \n"\
+            "{0}\n{1}\n".format("\n".join(errors), "\n".join(warnings))
+
+
+@pytest.fixture(scope="package")
+def acme(env):
+    acme_server = None
+    if env.acme_server == 'pebble':
+        acme_server = MDPebbleRunner(env, configs={
+            'default': os.path.join(env.gen_dir, 'pebble/pebble.json'),
+            'eab': os.path.join(env.gen_dir, 'pebble/pebble-eab.json'),
+        })
+    elif env.acme_server == 'boulder':
+        acme_server = MDBoulderRunner(env)
+    yield acme_server
+    if acme_server is not None:
+        acme_server.stop()
+

Propchange: httpd/httpd/branches/2.4.x/test/modules/md/conftest.py
------------------------------------------------------------------------------
    svn:executable = *

Added: httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/accounts/ACME-localhost-0000/account.json
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/accounts/ACME-localhost-0000/account.json?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/accounts/ACME-localhost-0000/account.json (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/accounts/ACME-localhost-0000/account.json Tue Dec 14 11:33:27 2021
@@ -0,0 +1,6 @@
+{
+  "disabled": false,
+  "url": "http://localhost:4000/acme/reg/494",
+  "ca-url": "http://localhost:4000/directory",
+  "id": "ACME-localhost-0000"
+}
\ No newline at end of file

Added: httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/accounts/ACME-localhost-0000/account.pem
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/accounts/ACME-localhost-0000/account.pem?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/accounts/ACME-localhost-0000/account.pem (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/accounts/ACME-localhost-0000/account.pem Tue Dec 14 11:33:27 2021
@@ -0,0 +1,54 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+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
+DWiLOvp6tLT58pX8TSlplbITcQ==
+-----END ENCRYPTED PRIVATE KEY-----

Added: httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/archive/7007-1502285564.org.1/md.json
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/archive/7007-1502285564.org.1/md.json?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/archive/7007-1502285564.org.1/md.json (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/archive/7007-1502285564.org.1/md.json Tue Dec 14 11:33:27 2021
@@ -0,0 +1,18 @@
+{
+  "name": "7007-1502285564.org",
+  "domains": [
+    "7007-1502285564.org"
+  ],
+  "contacts": [
+    "mailto:admin@7007-1502285564.org"
+  ],
+  "transitive": 0,
+  "ca": {
+    "proto": "ACME",
+    "url": "http://localhost:4000/directory",
+    "agreement": "http://boulder:4000/terms/v1"
+  },
+  "state": 1,
+  "renew-mode": 2,
+  "renew-window": 1209600
+}

Added: httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/cert.pem
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/cert.pem?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/cert.pem (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/cert.pem Tue Dec 14 11:33:27 2021
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFkDCCBHigAwIBAgITAP8PGcftT0j60OOjL+Er/XuHrzANBgkqhkiG9w0BAQsF
+ADAfMR0wGwYDVQQDDBRoMnBweSBoMmNrZXIgZmFrZSBDQTAeFw0xNzA4MDkxMjMz
+MDBaFw0xNzExMDcxMjMzMDBaME0xHDAaBgNVBAMTEzcwMDctMTUwMjI4NTU2NC5v
+cmcxLTArBgNVBAUTJGZmMGYxOWM3ZWQ0ZjQ4ZmFkMGUzYTMyZmUxMmJmZDdiODdh
+ZjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMHuhVxT9Jpc6EpNAhrq
+RqzDJ4tWSG9BtguKZzh3sbY92EE5rqym7wpdb5DG5gwew4iD1R+YizY+99+00qlB
+3kNBUVsJCBnew0apmhPq4jjF8v8t3Qqq0ISn2Sdv5bt5mB9NWeO83h3zT1LW0rTm
+847nwxUuGxlIjLXxsibUvPunMfyGJUshflN5V9/Q3YQBOCnDWy5s4FKN2N34cHFE
+IgJo5ToBKZLp9eUaLm03mlfhTFc3/h0AtWwMZ5P2tRRB9EiijqI9nkrVzqyi1QTN
+Hn/XfgDgKRCyMp6i5kcK3hCXo4GjOIU0KA91ttf3IeKhXHKzC7ybc4hdJH2rWzoN
+srYq6tNZ+cOaa1E/H+v+OMSeIRaRrpM56c3nUssIzbneMIXuLHuOluaaL4baCjYp
+Pdc80bUlps06XcnVHysAbsfbtWAtUdzj2l4flVySruGoaqVDudl1GqYoYa+0oReM
+Zqd09Q+pCQvDNE+jiVq3An+JA4msux9EMMz7jkAwnl8iiWy0GMuQPsL5gp3TEXGY
+Cp1wQlzpmxZSdUZ+J6f4UkFOS/Zn6gS6nSxN8nj3XKbRYRbebPQMwRGYGttCyeZO
+dHiUY/3gQBUdpcMBJhAa5GFoabK0J5XPmK2E1P9cGQo7DbNn+Skojnz2WuUtCuyo
+m9la14Ruca9V8NmjBsu+4mXvAgMBAAGjggGVMIIBkTAOBgNVHQ8BAf8EBAMCBaAw
+HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYD
+VR0OBBYEFH426IYgY0KXUe9cLMZ3d8tipsDkMB8GA1UdIwQYMBaAFPt4TxL5YBWD
+LJ8XfzQZsy426kGJMGYGCCsGAQUFBwEBBFowWDAiBggrBgEFBQcwAYYWaHR0cDov
+LzEyNy4wLjAuMTo0MDAyLzAyBggrBgEFBQcwAoYmaHR0cDovLzEyNy4wLjAuMTo0
+MDAwL2FjbWUvaXNzdWVyLWNlcnQwHgYDVR0RBBcwFYITNzAwNy0xNTAyMjg1NTY0
+Lm9yZzAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8vZXhhbXBsZS5jb20vY3JsMGEG
+A1UdIARaMFgwCAYGZ4EMAQIBMEwGAyoDBDBFMCIGCCsGAQUFBwIBFhZodHRwOi8v
+ZXhhbXBsZS5jb20vY3BzMB8GCCsGAQUFBwICMBMMEURvIFdoYXQgVGhvdSBXaWx0
+MA0GCSqGSIb3DQEBCwUAA4IBAQBfqLXSJZ5Izs2I44cXWrAto631aTylValp0Fiy
+Zz1dj00FS6XN5DGtfIyq7Ymd3MMiOZCLkTOMMb7BrJAvcgeJteKwdk3ffXEDyKH0
+1ttXK7l46trEyGOB+f9PMMKxVMyhDhGKyb6ro4Y5WTK/w4862soqKcP1SjHvk65u
+lIkFws1fWYYzqPLKLij2ILm+4NjdGIl8qPQWP2PtbOaDTFspJBz6hvLmqRgmjVVv
+cENwBUML4LCkVY3TUqoBHXDhpocTZlVeAVRVsroosboQJlY5nIKz6cOjilILn4cT
+hgEKa5IRwK5lUveCoeQtYUyLoyp5ncbota+UxNxCnkl/0veK
+-----END CERTIFICATE-----

Added: httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/chain.pem
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/chain.pem?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/chain.pem (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/chain.pem Tue Dec 14 11:33:27 2021
@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE-----
+MIIEijCCA3KgAwIBAgICEk0wDQYJKoZIhvcNAQELBQAwKzEpMCcGA1UEAwwgY2Fj
+a2xpbmcgY3J5cHRvZ3JhcGhlciBmYWtlIFJPT1QwHhcNMTUxMDIxMjAxMTUyWhcN
+MjAxMDE5MjAxMTUyWjAfMR0wGwYDVQQDExRoYXBweSBoYWNrZXIgZmFrZSBDQTCC
+ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMIKR3maBcUSsncXYzQT13D5
+Nr+Z3mLxMMh3TUdt6sACmqbJ0btRlgXfMtNLM2OU1I6a3Ju+tIZSdn2v21JBwvxU
+zpZQ4zy2cimIiMQDZCQHJwzC9GZn8HaW091iz9H0Go3A7WDXwYNmsdLNRi00o14U
+joaVqaPsYrZWvRKaIRqaU0hHmS0AWwQSvN/93iMIXuyiwywmkwKbWnnxCQ/gsctK
+FUtcNrwEx9Wgj6KlhwDTyI1QWSBbxVYNyUgPFzKxrSmwMO0yNff7ho+QT9x5+Y/7
+XE59S4Mc4ZXxcXKew/gSlN9U5mvT+D2BhDtkCupdfsZNCQWp27A+b/DmrFI9NqsC
+AwEAAaOCAcIwggG+MBIGA1UdEwEB/wQIMAYBAf8CAQAwQwYDVR0eBDwwOqE4MAaC
+BC5taWwwCocIAAAAAAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAwDgYDVR0PAQH/BAQDAgGGMH8GCCsGAQUFBwEBBHMwcTAyBggrBgEFBQcw
+AYYmaHR0cDovL2lzcmcudHJ1c3RpZC5vY3NwLmlkZW50cnVzdC5jb20wOwYIKwYB
+BQUHMAKGL2h0dHA6Ly9hcHBzLmlkZW50cnVzdC5jb20vcm9vdHMvZHN0cm9vdGNh
+eDMucDdjMB8GA1UdIwQYMBaAFOmkP+6epeby1dd5YDyTpi4kjpeqMFQGA1UdIARN
+MEswCAYGZ4EMAQIBMD8GCysGAQQBgt8TAQEBMDAwLgYIKwYBBQUHAgEWImh0dHA6
+Ly9jcHMucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcwPAYDVR0fBDUwMzAxoC+gLYYr
+aHR0cDovL2NybC5pZGVudHJ1c3QuY29tL0RTVFJPT1RDQVgzQ1JMLmNybDAdBgNV
+HQ4EFgQU+3hPEvlgFYMsnxd/NBmzLjbqQYkwDQYJKoZIhvcNAQELBQADggEBAA0Y
+AeLXOklx4hhCikUUl+BdnFfn1g0W5AiQLVNIOL6PnqXu0wjnhNyhqdwnfhYMnoy4
+idRh4lB6pz8Gf9pnlLd/DnWSV3gS+/I/mAl1dCkKby6H2V790e6IHmIK2KYm3jm+
+U++FIdGpBdsQTSdmiX/rAyuxMDM0adMkNBwTfQmZQCz6nGHw1QcSPZMvZpsC8Skv
+ekzxsjF1otOrMUPNPQvtTWrVx8GlR2qfx/4xbQa1v2frNvFBCmO59goz+jnWvfTt
+j2NjwDZ7vlMBsPm16dbKYC840uvRoZjxqsdc3ChCZjqimFqlNG/xoPA8+dTicZzC
+XE9ijPIcvW6y1aa3bGw=
+-----END CERTIFICATE-----

Added: httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/md.json
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/md.json?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/md.json (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/md.json Tue Dec 14 11:33:27 2021
@@ -0,0 +1,23 @@
+{
+  "name": "7007-1502285564.org",
+  "domains": [
+    "7007-1502285564.org"
+  ],
+  "contacts": [
+    "mailto:admin@7007-1502285564.org"
+  ],
+  "transitive": 0,
+  "ca": {
+    "account": "ACME-localhost-0000",
+    "proto": "ACME",
+    "url": "http://localhost:4000/directory",
+    "agreement": "http://boulder:4000/terms/v1"
+  },
+  "cert": {
+    "url": "http://localhost:4000/acme/cert/ff0f19c7ed4f48fad0e3a32fe12bfd7b87af",
+    "expires": "Tue, 07 Nov 2017 12:33:00 GMT"
+  },
+  "state": 2,
+  "renew-mode": 2,
+  "renew-window": 1209600
+}

Added: httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/pkey.pem
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/pkey.pem?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/pkey.pem (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/domains/7007-1502285564.org/pkey.pem Tue Dec 14 11:33:27 2021
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+Wc8hH7giOwSubI2D2mauspM5CZlez7A=
+-----END PRIVATE KEY-----

Added: httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/httpd.json
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/httpd.json?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/httpd.json (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/httpd.json Tue Dec 14 11:33:27 2021
@@ -0,0 +1,6 @@
+{
+  "proto": {
+    "http": true,
+    "https": true
+  }
+}
\ No newline at end of file

Added: httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/md_store.json
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/md_store.json?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/md_store.json (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/data/store_migrate/1.0/sample1/md_store.json Tue Dec 14 11:33:27 2021
@@ -0,0 +1,7 @@
+{
+  "version": "0.6.1-git",
+  "store": {
+    "version": 1.0
+  },
+  "key": "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXphYmNkZWZnaGlqa2xtbm9wcXJzdHV2"
+}
\ No newline at end of file

Added: httpd/httpd/branches/2.4.x/test/modules/md/data/test_920/002.pubcert
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/data/test_920/002.pubcert?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/data/test_920/002.pubcert (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/data/test_920/002.pubcert Tue Dec 14 11:33:27 2021
@@ -0,0 +1,58 @@
+-----BEGIN CERTIFICATE-----
+MIIFYDCCBEigAwIBAgISAwOcRk1FTt55/NLK6Fn2aPJpMA0GCSqGSIb3DQEBCwUA
+MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
+ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xOTA1MzExNjA2MzVaFw0x
+OTA4MjkxNjA2MzVaMBYxFDASBgNVBAMTC2Vpc3Npbmcub3JnMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9d5xZdknImIPfmiUaiiRhHLx4bvazWRTgA2+
+etRNKr42MRjkuLbAhvxGjhw4El0GJlbngKTfiSK0Vq0idW/ehUr++czRSDrRVfqq
+qcI/F4NXLIbIZfmR7/vG0IP8Xc8D9VyQCX0uDapCvw+A/U46p0VOZz4bIB/bl0BW
+/mqBvVhBU9owskUcPjwwI/tK6My933CUVKXuFpPZ4V7zoY0/8Xa6JmWC2q1+7XmE
+h51hPnU35dYH1bA7WblX8rVxnEPCyCOgABVLKb6NhWfTCEqy+yzr32KsoSR1xqe4
+T2EeTcoamwF2yhz2zRC4glX0LM4inJ1/ZOQ+nKbFZTOPVWEnLQIDAQABo4ICcjCC
+Am4wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
+AjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTfO7pZGPLsa0NuPZMG4NGlr1TaWjAf
+BgNVHSMEGDAWgBSoSmpjBH3duubRObemRWXv86jsoTBvBggrBgEFBQcBAQRjMGEw
+LgYIKwYBBQUHMAGGImh0dHA6Ly9vY3NwLmludC14My5sZXRzZW5jcnlwdC5vcmcw
+LwYIKwYBBQUHMAKGI2h0dHA6Ly9jZXJ0LmludC14My5sZXRzZW5jcnlwdC5vcmcv
+MCcGA1UdEQQgMB6CC2Vpc3Npbmcub3Jngg93d3cuZWlzc2luZy5vcmcwTAYDVR0g
+BEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0
+cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwggEFBgorBgEEAdZ5AgQCBIH2BIHzAPEA
+dwB0ftqDMa0zEJEhnM4lT0Jwwr/9XkIgCMY3NXnmEHvMVgAAAWsO24QlAAAEAwBI
+MEYCIQD8yd2uHl2DNgvnBkSiA8vsK5pOv204NixI9F89LWERwgIhAPMLLiZkFG2h
+DTpEwF50BbZ+laYH8VP03Teq5csk2lX0AHYAKTxRllTIOWW6qlD8WAfUt2+/WHop
+ctykwwz05UVH9HgAAAFrDtuEFgAABAMARzBFAiEA3bYpKSNigSe0HuDyH/kerTW2
+55ugvODp6d+vNbNmgZoCIGTd4cio769BTKfLJTqNbjc9sKK9T7XkHUO4JgQdY6Nq
+MA0GCSqGSIb3DQEBCwUAA4IBAQBeatZxh8leVmeFE/IYTKKqHyZqTccJKdugXIOr
+uIF6sLup/8Fv/2N0wZc+edkj+NCyWhxxkZULyW6xhlL7rtzcwLYbQBSxKvT4Utur
+01a5bwhM62MdMjzkFgCCa5nRKPQ7bc684RrUFNi94d0KSb5ArFv8wovqPW7jbmFp
+X50dYKCE+wohFPHcsQapnV0lXK4+5qJZSZkp/pHANdndLCvFfzRHhV4nqRA12G2T
+VVWjdHN6ShL2uykJVAnSBhu/XD4mh79Yq9TQtS1DHfP3HcKstLqR0nrwBFaB6087
+jXfIpJ46yObq001qHeUMhT+B3WI2YPp/hY7u8A9+hCmDyyq8
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
+MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
+DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow
+SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT
+GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF
+q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8
+SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0
+Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA
+a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj
+/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T
+AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG
+CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv
+bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k
+c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw
+VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC
+ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz
+MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu
+Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF
+AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo
+uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/
+wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu
+X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG
+PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
+KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
+-----END CERTIFICATE-----

Added: httpd/httpd/branches/2.4.x/test/modules/md/data/test_conf_validate/test_014.conf
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/data/test_conf_validate/test_014.conf?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/data/test_conf_validate/test_014.conf (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/data/test_conf_validate/test_014.conf Tue Dec 14 11:33:27 2021
@@ -0,0 +1,8 @@
+# global server name as managed domain name
+
+MDomain resistance.fritz.box www.example2.org
+
+<VirtualHost *:12346>
+    ServerName www.example2.org
+
+</VirtualHost>

Added: httpd/httpd/branches/2.4.x/test/modules/md/data/test_drive/test1.example.org.conf
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/data/test_drive/test1.example.org.conf?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/data/test_drive/test1.example.org.conf (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/data/test_drive/test1.example.org.conf Tue Dec 14 11:33:27 2021
@@ -0,0 +1,6 @@
+# A setup that required manual driving, e.g. invoking a2md outside apache
+#
+MDRenewMode manual
+
+MDomain test1.not-forbidden.org www.test1.not-forbidden.org mail.test1.not-forbidden.org
+

Added: httpd/httpd/branches/2.4.x/test/modules/md/data/test_roundtrip/temp.conf
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/data/test_roundtrip/temp.conf?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/data/test_roundtrip/temp.conf (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/data/test_roundtrip/temp.conf Tue Dec 14 11:33:27 2021
@@ -0,0 +1,27 @@
+  MDDriveMode manual
+  MDCertificateAuthority http://localhost:4000/directory
+  MDCertificateProtocol ACME
+  MDCertificateAgreement http://boulder:4000/terms/v1
+
+  ServerAdmin mailto:admin@test102-1499953506.org
+
+  ManagedDomain test102-1499953506.org test-a.test102-1499953506.org test-b.test102-1499953506.org
+
+<VirtualHost *:5001>
+    ServerName test-a.test102-1499953506.org
+    DocumentRoot htdocs/a
+
+    SSLEngine on
+    SSLCertificateFile /Users/sei/projects/mod_md/test/gen/apache/md/domains/test102-1499953506.org/cert.pem
+    SSLCertificateKeyFile /Users/sei/projects/mod_md/test/gen/apache/md/domains/test102-1499953506.org/pkey.pem
+</VirtualHost>
+
+<VirtualHost *:5001>
+    ServerName test-b.test102-1499953506.org
+    DocumentRoot htdocs/b
+
+    SSLEngine on
+    SSLCertificateFile /Users/sei/projects/mod_md/test/gen/apache/md/domains/test102-1499953506.org/cert.pem
+    SSLCertificateKeyFile /Users/sei/projects/mod_md/test/gen/apache/md/domains/test102-1499953506.org/pkey.pem
+</VirtualHost>
+

Added: httpd/httpd/branches/2.4.x/test/modules/md/dns01.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/dns01.py?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/dns01.py (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/dns01.py Tue Dec 14 11:33:27 2021
@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+
+import subprocess
+import sys
+
+curl = "curl"
+challtestsrv = "localhost:8055"
+
+
+def run(args):
+    sys.stderr.write(f"run: {' '.join(args)}\n")
+    p = subprocess.Popen(args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    output, errput = p.communicate(None)
+    rv = p.wait()
+    if rv != 0:
+        sys.stderr.write(errput.decode())
+    sys.stdout.write(output.decode())
+    return rv
+
+
+def teardown(domain):
+    rv = run([curl, '-s', '-d', f'{{"host":"_acme-challenge.{domain}"}}',
+              f'{challtestsrv}/clear-txt'])
+    if rv == 0:
+        rv = run([curl, '-s', '-d', f'{{"host":"{domain}"}}',
+                  f'{challtestsrv}/set-txt'])
+    return rv
+
+
+def setup(domain, challenge):
+    teardown(domain)
+    rv = run([curl, '-s', '-d', f'{{"host":"{domain}", "addresses":["127.0.0.1"]}}',
+              f'{challtestsrv}/set-txt'])
+    if rv == 0:
+        rv = run([curl, '-s', '-d', f'{{"host":"_acme-challenge.{domain}.", "value":"{challenge}"}}',
+                  f'{challtestsrv}/set-txt'])
+    return rv
+
+
+def main(argv):
+    if len(argv) > 1:
+        if argv[1] == 'setup':
+            if len(argv) != 4:
+                sys.stderr.write("wrong number of arguments: dns01.py setup <domain> <challenge>\n")
+                sys.exit(2)
+            rv = setup(argv[2], argv[3])
+        elif argv[1] == 'teardown':
+            if len(argv) != 3:
+                sys.stderr.write("wrong number of arguments: dns01.py teardown <domain>\n")
+                sys.exit(1)
+            rv = teardown(argv[2])
+        else:
+            sys.stderr.write(f"unknown option {argv[1]}\n")
+            rv = 2
+    else:
+        sys.stderr.write("dns01.py wrong number of arguments\n")
+        rv = 2
+    sys.exit(rv)
+    
+
+if __name__ == "__main__":
+    main(sys.argv)

Propchange: httpd/httpd/branches/2.4.x/test/modules/md/dns01.py
------------------------------------------------------------------------------
    svn:executable = *

Added: httpd/httpd/branches/2.4.x/test/modules/md/http_challenge_foobar.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/http_challenge_foobar.py?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/http_challenge_foobar.py (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/http_challenge_foobar.py Tue Dec 14 11:33:27 2021
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+import os
+import re
+import sys
+
+
+def main(argv):
+    if len(argv) < 4:
+        sys.stderr.write(f"{argv[0]} without too few arguments")
+        sys.exit(7)
+    store_dir = argv[1]
+    event = argv[2]
+    mdomain = argv[3]
+    m = re.match(r'(\S+):(\S+):(\S+)', event)
+    if m and 'challenge-setup' == m.group(1) and 'http-01' == m.group(2):
+        dns_name = m.group(3)
+        challenge_file = f"{store_dir}/challenges/{dns_name}/acme-http-01.txt"
+        if not os.path.isfile(challenge_file):
+            sys.stderr.write(f"{argv[0]} does not exist: {challenge_file}")
+            sys.exit(8)
+        with open(challenge_file, 'w') as fd:
+            fd.write('this_is_an_invalidated_http-01_challenge')
+    sys.exit(0)
+
+
+if __name__ == "__main__":
+    main(sys.argv)

Propchange: httpd/httpd/branches/2.4.x/test/modules/md/http_challenge_foobar.py
------------------------------------------------------------------------------
    svn:executable = *

Added: httpd/httpd/branches/2.4.x/test/modules/md/md_acme.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/md_acme.py?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/md_acme.py (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/md_acme.py Tue Dec 14 11:33:27 2021
@@ -0,0 +1,125 @@
+import logging
+import os
+import shutil
+import subprocess
+import time
+from abc import ABCMeta, abstractmethod
+from datetime import datetime, timedelta
+from threading import Thread
+from typing import Dict
+
+from .md_env import MDTestEnv
+
+
+log = logging.getLogger(__name__)
+
+
+def monitor_proc(env: MDTestEnv, proc):
+    _env = env
+    proc.wait()
+
+
+class ACMEServer:
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def start(self):
+        raise NotImplementedError
+
+    @abstractmethod
+    def stop(self):
+        raise NotImplementedError
+
+    @abstractmethod
+    def install_ca_bundle(self, dest):
+        raise NotImplementedError
+
+
+class MDPebbleRunner(ACMEServer):
+
+    def __init__(self, env: MDTestEnv, configs: Dict[str, str]):
+        self.env = env
+        self.configs = configs
+        self._current = 'default'
+        self._pebble = None
+        self._challtestsrv = None
+        self._log = None
+
+    def start(self, config: str = None):
+        if config is not None and config != self._current:
+            # change, tear down and start again
+            assert config in self.configs
+            self.stop()
+            self._current = config
+        elif self._pebble is not None:
+            # already running
+            return
+        args = ['pebble', '-config', self.configs[self._current], '-dnsserver', ':8053']
+        env = {}
+        env.update(os.environ)
+        env['PEBBLE_VA_NOSLEEP'] = '1'
+        self._log = open(f'{self.env.gen_dir}/pebble.log', 'w')
+        self._pebble = subprocess.Popen(args=args, env=env,
+                                        stdout=self._log, stderr=self._log)
+        t = Thread(target=monitor_proc, args=(self.env, self._pebble))
+        t.start()
+
+        args = ['pebble-challtestsrv', '-http01', '', '-https01', '', '-tlsalpn01', '']
+        self._challtestsrv = subprocess.Popen(args, stdout=self._log, stderr=self._log)
+        t = Thread(target=monitor_proc, args=(self.env, self._challtestsrv))
+        t.start()
+        self.install_ca_bundle(self.env.acme_ca_pemfile)
+        # disable ipv6 default address, this gives trouble inside docker
+        end = datetime.now() + timedelta(seconds=5)
+        while True:
+            r = self.env.run(['curl', 'localhost:8055/'])
+            if r.exit_code == 0:
+                break
+            if datetime.now() > end:
+                raise TimeoutError(f'unable to contact pebble-challtestsrv on localhost:8055')
+            time.sleep(.1)
+        r = self.env.run(['curl', '-d', f'{{"ip":""}}',
+                          'localhost:8055/set-default-ipv6'])
+        assert r.exit_code == 0, f"{r}"
+
+    def stop(self):
+        if self._pebble:
+            self._pebble.terminate()
+            self._pebble = None
+        if self._challtestsrv:
+            self._challtestsrv.terminate()
+            self._challtestsrv = None
+        if self._log:
+            self._log.close()
+            self._log = None
+
+    def install_ca_bundle(self, dest):
+        shutil.copyfile(self.env.ca.cert_file, dest)
+        end = datetime.now() + timedelta(seconds=20)
+        while datetime.now() < end:
+            r = self.env.curl_get('https://localhost:15000/roots/0', insecure=True)
+            if r.exit_code == 0:
+                with open(dest, 'a') as fd:
+                    fd.write(r.stdout)
+                break
+
+
+class MDBoulderRunner(ACMEServer):
+
+    def __init__(self, env: MDTestEnv):
+        self.env = env
+        self.install_ca_bundle(self.env.acme_ca_pemfile)
+
+    def start(self, config=None):
+        pass
+
+    def stop(self):
+        pass
+
+    def install_ca_bundle(self, dest):
+        r = self.env.run([
+            'docker', 'exec', 'boulder_boulder_1', 'bash', '-c', "cat /tmp/root*.pem"
+        ])
+        assert r.exit_code == 0
+        with open(dest, 'w') as fd:
+            fd.write(r.stdout)

Propchange: httpd/httpd/branches/2.4.x/test/modules/md/md_acme.py
------------------------------------------------------------------------------
    svn:executable = *

Added: httpd/httpd/branches/2.4.x/test/modules/md/md_cert_util.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/md_cert_util.py?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/md_cert_util.py (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/md_cert_util.py Tue Dec 14 11:33:27 2021
@@ -0,0 +1,239 @@
+import logging
+import re
+import os
+import socket
+import OpenSSL
+import time
+import sys
+
+from datetime import datetime
+from datetime import tzinfo
+from datetime import timedelta
+from http.client import HTTPConnection
+from urllib.parse import urlparse
+
+
+SEC_PER_DAY = 24 * 60 * 60
+
+
+log = logging.getLogger(__name__)
+
+
+class MDCertUtil(object):
+    # Utility class for inspecting certificates in test cases
+    # Uses PyOpenSSL: https://pyopenssl.org/en/stable/index.html
+
+    @classmethod
+    def create_self_signed_cert(cls, path, name_list, valid_days, serial=1000):
+        domain = name_list[0]
+        if not os.path.exists(path):
+            os.makedirs(path)
+
+        cert_file = os.path.join(path, 'pubcert.pem')
+        pkey_file = os.path.join(path, 'privkey.pem')
+        # create a key pair
+        if os.path.exists(pkey_file):
+            key_buffer = open(pkey_file, 'rt').read()
+            k = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, key_buffer)
+        else:
+            k = OpenSSL.crypto.PKey()
+            k.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
+
+        # create a self-signed cert
+        cert = OpenSSL.crypto.X509()
+        cert.get_subject().C = "DE"
+        cert.get_subject().ST = "NRW"
+        cert.get_subject().L = "Muenster"
+        cert.get_subject().O = "greenbytes GmbH"
+        cert.get_subject().CN = domain
+        cert.set_serial_number(serial)
+        cert.gmtime_adj_notBefore(valid_days["notBefore"] * SEC_PER_DAY)
+        cert.gmtime_adj_notAfter(valid_days["notAfter"] * SEC_PER_DAY)
+        cert.set_issuer(cert.get_subject())
+
+        cert.add_extensions([OpenSSL.crypto.X509Extension(
+            b"subjectAltName", False, b", ".join(map(lambda n: b"DNS:" + n.encode(), name_list))
+        )])
+        cert.set_pubkey(k)
+        cert.sign(k, 'sha1')
+
+        open(cert_file, "wt").write(
+            OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert).decode('utf-8'))
+        open(pkey_file, "wt").write(
+            OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, k).decode('utf-8'))
+
+    @classmethod
+    def load_server_cert(cls, host_ip, host_port, host_name, tls=None, ciphers=None):
+        ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
+        if tls is not None and tls != 1.0:
+            ctx.set_options(OpenSSL.SSL.OP_NO_TLSv1)
+        if tls is not None and tls != 1.1:
+            ctx.set_options(OpenSSL.SSL.OP_NO_TLSv1_1)
+        if tls is not None and tls != 1.2:
+            ctx.set_options(OpenSSL.SSL.OP_NO_TLSv1_2)
+        if tls is not None and tls != 1.3 and hasattr(OpenSSL.SSL, "OP_NO_TLSv1_3"):
+            ctx.set_options(OpenSSL.SSL.OP_NO_TLSv1_3)
+        if ciphers is not None:
+            ctx.set_cipher_list(ciphers)
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        connection = OpenSSL.SSL.Connection(ctx, s)
+        connection.connect((host_ip, int(host_port)))
+        connection.setblocking(1)
+        connection.set_tlsext_host_name(host_name.encode('utf-8'))
+        connection.do_handshake()
+        peer_cert = connection.get_peer_certificate()
+        return MDCertUtil(None, cert=peer_cert)
+
+    @classmethod
+    def parse_pem_cert(cls, text):
+        cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, text.encode('utf-8'))
+        return MDCertUtil(None, cert=cert)
+
+    @classmethod
+    def get_plain(cls, url, timeout):
+        server = urlparse(url)
+        try_until = time.time() + timeout
+        while time.time() < try_until:
+            # noinspection PyBroadException
+            try:
+                c = HTTPConnection(server.hostname, server.port, timeout=timeout)
+                c.request('GET', server.path)
+                resp = c.getresponse()
+                data = resp.read()
+                c.close()
+                return data
+            except IOError:
+                log.debug("connect error:", sys.exc_info()[0])
+                time.sleep(.1)
+            except:
+                log.error("Unexpected error:", sys.exc_info()[0])
+        log.error("Unable to contact server after %d sec" % timeout)
+        return None
+
+    def __init__(self, cert_path, cert=None):
+        if cert_path is not None:
+            self.cert_path = cert_path
+            # load certificate and private key
+            if cert_path.startswith("http"):
+                cert_data = self.get_plain(cert_path, 1)
+            else:
+                cert_data = MDCertUtil._load_binary_file(cert_path)
+
+            for file_type in (OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1):
+                try:
+                    self.cert = OpenSSL.crypto.load_certificate(file_type, cert_data)
+                except Exception as error:
+                    self.error = error
+        if cert is not None:
+            self.cert = cert
+
+        if self.cert is None:
+            raise self.error
+
+    def get_issuer(self):
+        return self.cert.get_issuer()
+
+    def get_serial(self):
+        # the string representation of a serial number is not unique. Some
+        # add leading 0s to align with word boundaries.
+        return ("%lx" % (self.cert.get_serial_number())).upper()
+
+    def same_serial_as(self, other):
+        if isinstance(other, MDCertUtil):
+            return self.cert.get_serial_number() == other.cert.get_serial_number()
+        elif isinstance(other, OpenSSL.crypto.X509):
+            return self.cert.get_serial_number() == other.get_serial_number()
+        elif isinstance(other, str):
+            # assume a hex number
+            return self.cert.get_serial_number() == int(other, 16)
+        elif isinstance(other, int):
+            return self.cert.get_serial_number() == other
+        return False
+
+    def get_not_before(self):
+        tsp = self.cert.get_notBefore()
+        return self._parse_tsp(tsp)
+
+    def get_not_after(self):
+        tsp = self.cert.get_notAfter()
+        return self._parse_tsp(tsp)
+
+    def get_cn(self):
+        return self.cert.get_subject().CN
+
+    def get_key_length(self):
+        return self.cert.get_pubkey().bits()
+
+    def get_san_list(self):
+        text = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_TEXT, self.cert).decode("utf-8")
+        m = re.search(r"X509v3 Subject Alternative Name:\s*(.*)", text)
+        sans_list = []
+        if m:
+            sans_list = m.group(1).split(",")
+
+        def _strip_prefix(s):
+            return s.split(":")[1] if s.strip().startswith("DNS:") else s.strip()
+        return list(map(_strip_prefix, sans_list))
+
+    def get_must_staple(self):
+        text = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_TEXT, self.cert).decode("utf-8")
+        m = re.search(r"1.3.6.1.5.5.7.1.24:\s*\n\s*0....", text)
+        if not m:
+            # Newer openssl versions print this differently
+            m = re.search(r"TLS Feature:\s*\n\s*status_request\s*\n", text)
+        return m is not None
+
+    @classmethod
+    def validate_privkey(cls, privkey_path, passphrase=None):
+        privkey_data = cls._load_binary_file(privkey_path)
+        if passphrase:
+            privkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey_data, passphrase)
+        else:
+            privkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey_data)
+        return privkey.check()
+
+    def validate_cert_matches_priv_key(self, privkey_path):
+        # Verifies that the private key and cert match.
+        privkey_data = MDCertUtil._load_binary_file(privkey_path)
+        privkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey_data)
+        context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
+        context.use_privatekey(privkey)
+        context.use_certificate(self.cert)
+        context.check_privatekey()
+
+    # --------- _utils_ ---------
+
+    def astr(self, s):
+        return s.decode('utf-8')
+        
+    def _parse_tsp(self, tsp):
+        # timestampss returned by PyOpenSSL are bytes
+        # parse date and time part
+        s = ("%s-%s-%s %s:%s:%s" % (self.astr(tsp[0:4]), self.astr(tsp[4:6]), self.astr(tsp[6:8]),
+                                    self.astr(tsp[8:10]), self.astr(tsp[10:12]), self.astr(tsp[12:14])))
+        timestamp = datetime.strptime(s, '%Y-%m-%d %H:%M:%S')
+        # adjust timezone
+        tz_h, tz_m = 0, 0
+        m = re.match(r"([+\-]\d{2})(\d{2})", self.astr(tsp[14:]))
+        if m:
+            tz_h, tz_m = int(m.group(1)),  int(m.group(2)) if tz_h > 0 else -1 * int(m.group(2))
+        return timestamp.replace(tzinfo=self.FixedOffset(60 * tz_h + tz_m))
+
+    @classmethod
+    def _load_binary_file(cls, path):
+        with open(path, mode="rb") as file:
+            return file.read()
+
+    class FixedOffset(tzinfo):
+
+        def __init__(self, offset):
+            self.__offset = timedelta(minutes=offset)
+
+        def utcoffset(self, dt):
+            return self.__offset
+
+        def tzname(self, dt):
+            return None
+
+        def dst(self, dt):
+            return timedelta(0)

Propchange: httpd/httpd/branches/2.4.x/test/modules/md/md_cert_util.py
------------------------------------------------------------------------------
    svn:executable = *

Added: httpd/httpd/branches/2.4.x/test/modules/md/md_certs.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/md_certs.py?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/md_certs.py (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/md_certs.py Tue Dec 14 11:33:27 2021
@@ -0,0 +1,444 @@
+import os
+import re
+from datetime import timedelta, datetime
+from typing import List, Any, Optional
+
+from cryptography import x509
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.asymmetric import ec, rsa
+from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
+from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
+from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption, load_pem_private_key
+from cryptography.x509 import ExtendedKeyUsageOID, NameOID
+
+
+EC_SUPPORTED = {}
+EC_SUPPORTED.update([(curve.name.upper(), curve) for curve in [
+    ec.SECP192R1,
+    ec.SECP224R1,
+    ec.SECP256R1,
+    ec.SECP384R1,
+]])
+
+
+def _private_key(key_type):
+    if isinstance(key_type, str):
+        key_type = key_type.upper()
+        m = re.match(r'^(RSA)?(\d+)$', key_type)
+        if m:
+            key_type = int(m.group(2))
+
+    if isinstance(key_type, int):
+        return rsa.generate_private_key(
+            public_exponent=65537,
+            key_size=key_type,
+            backend=default_backend()
+        )
+    if not isinstance(key_type, ec.EllipticCurve) and key_type in EC_SUPPORTED:
+        key_type = EC_SUPPORTED[key_type]
+    return ec.generate_private_key(
+        curve=key_type,
+        backend=default_backend()
+    )
+
+
+class CertificateSpec:
+
+    def __init__(self, name: str = None, domains: List[str] = None,
+                 email: str = None,
+                 key_type: str = None, single_file: bool = False,
+                 valid_from: timedelta = timedelta(days=-1),
+                 valid_to: timedelta = timedelta(days=89),
+                 client: bool = False,
+                 sub_specs: List['CertificateSpec'] = None):
+        self._name = name
+        self.domains = domains
+        self.client = client
+        self.email = email
+        self.key_type = key_type
+        self.single_file = single_file
+        self.valid_from = valid_from
+        self.valid_to = valid_to
+        self.sub_specs = sub_specs
+
+    @property
+    def name(self) -> Optional[str]:
+        if self._name:
+            return self._name
+        elif self.domains:
+            return self.domains[0]
+        return None
+
+
+class Credentials:
+
+    def __init__(self, name: str, cert: Any, pkey: Any):
+        self._name = name
+        self._cert = cert
+        self._pkey = pkey
+        self._cert_file = None
+        self._pkey_file = None
+        self._store = None
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def subject(self) -> x509.Name:
+        return self._cert.subject
+
+    @property
+    def key_type(self):
+        if isinstance(self._pkey, RSAPrivateKey):
+            return f"rsa{self._pkey.key_size}"
+        elif isinstance(self._pkey, EllipticCurvePrivateKey):
+            return f"{self._pkey.curve.name}"
+        else:
+            raise Exception(f"unknown key type: {self._pkey}")
+
+    @property
+    def private_key(self) -> Any:
+        return self._pkey
+
+    @property
+    def certificate(self) -> Any:
+        return self._cert
+
+    @property
+    def cert_pem(self) -> bytes:
+        return self._cert.public_bytes(Encoding.PEM)
+
+    @property
+    def pkey_pem(self) -> bytes:
+        return self._pkey.private_bytes(
+            Encoding.PEM,
+            PrivateFormat.TraditionalOpenSSL if self.key_type.startswith('rsa') else PrivateFormat.PKCS8,
+            NoEncryption())
+
+    def set_store(self, store: 'CertStore'):
+        self._store = store
+
+    def set_files(self, cert_file: str, pkey_file: str = None):
+        self._cert_file = cert_file
+        self._pkey_file = pkey_file
+
+    @property
+    def cert_file(self) -> str:
+        return self._cert_file
+
+    @property
+    def pkey_file(self) -> Optional[str]:
+        return self._pkey_file
+
+    def get_first(self, name) -> Optional['Credentials']:
+        creds = self._store.get_credentials_for_name(name) if self._store else []
+        return creds[0] if len(creds) else None
+
+    def get_credentials_for_name(self, name) -> List['Credentials']:
+        return self._store.get_credentials_for_name(name) if self._store else []
+
+    def issue_certs(self, specs: List[CertificateSpec],
+                    chain: List['Credentials'] = None) -> List['Credentials']:
+        return [self.issue_cert(spec=spec, chain=chain) for spec in specs]
+
+    def issue_cert(self, spec: CertificateSpec, chain: List['Credentials'] = None) -> 'Credentials':
+        key_type = spec.key_type if spec.key_type else self.key_type
+        creds = self._store.load_credentials(name=spec.name, key_type=key_type, single_file=spec.single_file) \
+            if self._store else None
+        if creds is None:
+            creds = MDTestCA.create_credentials(spec=spec, issuer=self, key_type=key_type,
+                                                valid_from=spec.valid_from, valid_to=spec.valid_to)
+            if self._store:
+                self._store.save(creds, single_file=spec.single_file)
+
+        if spec.sub_specs:
+            if self._store:
+                sub_store = CertStore(fpath=os.path.join(self._store.path, creds.name))
+                creds.set_store(sub_store)
+            subchain = chain.copy() if chain else []
+            subchain.append(self)
+            creds.issue_certs(spec.sub_specs, chain=subchain)
+        return creds
+
+
+class CertStore:
+
+    def __init__(self, fpath: str):
+        self._store_dir = fpath
+        if not os.path.exists(self._store_dir):
+            os.makedirs(self._store_dir)
+        self._creds_by_name = {}
+
+    @property
+    def path(self) -> str:
+        return self._store_dir
+
+    def save(self, creds: Credentials, name: str = None,
+             chain: List[Credentials] = None,
+             single_file: bool = False) -> None:
+        name = name if name is not None else creds.name
+        cert_file = self.get_cert_file(name=name, key_type=creds.key_type)
+        pkey_file = self.get_pkey_file(name=name, key_type=creds.key_type)
+        if single_file:
+            pkey_file = None
+        with open(cert_file, "wb") as fd:
+            fd.write(creds.cert_pem)
+            if chain:
+                for c in chain:
+                    fd.write(c.cert_pem)
+            if pkey_file is None:
+                fd.write(creds.pkey_pem)
+        if pkey_file is not None:
+            with open(pkey_file, "wb") as fd:
+                fd.write(creds.pkey_pem)
+        creds.set_files(cert_file, pkey_file)
+        self._add_credentials(name, creds)
+
+    def _add_credentials(self, name: str, creds: Credentials):
+        if name not in self._creds_by_name:
+            self._creds_by_name[name] = []
+        self._creds_by_name[name].append(creds)
+
+    def get_credentials_for_name(self, name) -> List[Credentials]:
+        return self._creds_by_name[name] if name in self._creds_by_name else []
+
+    def get_cert_file(self, name: str, key_type=None) -> str:
+        key_infix = ".{0}".format(key_type) if key_type is not None else ""
+        return os.path.join(self._store_dir, f'{name}{key_infix}.cert.pem')
+
+    def get_pkey_file(self, name: str, key_type=None) -> str:
+        key_infix = ".{0}".format(key_type) if key_type is not None else ""
+        return os.path.join(self._store_dir, f'{name}{key_infix}.pkey.pem')
+
+    def load_pem_cert(self, fpath: str) -> x509.Certificate:
+        with open(fpath) as fd:
+            return x509.load_pem_x509_certificate("".join(fd.readlines()).encode())
+
+    def load_pem_pkey(self, fpath: str):
+        with open(fpath) as fd:
+            return load_pem_private_key("".join(fd.readlines()).encode(), password=None)
+
+    def load_credentials(self, name: str, key_type=None, single_file: bool = False):
+        cert_file = self.get_cert_file(name=name, key_type=key_type)
+        pkey_file = cert_file if single_file else self.get_pkey_file(name=name, key_type=key_type)
+        if os.path.isfile(cert_file) and os.path.isfile(pkey_file):
+            cert = self.load_pem_cert(cert_file)
+            pkey = self.load_pem_pkey(pkey_file)
+            creds = Credentials(name=name, cert=cert, pkey=pkey)
+            creds.set_store(self)
+            creds.set_files(cert_file, pkey_file)
+            self._add_credentials(name, creds)
+            return creds
+        return None
+
+
+class MDTestCA:
+
+    @classmethod
+    def create_root(cls, name: str, store_dir: str, key_type: str = "rsa2048") -> Credentials:
+        store = CertStore(fpath=store_dir)
+        creds = store.load_credentials(name="ca", key_type=key_type)
+        if creds is None:
+            creds = MDTestCA._make_ca_credentials(name=name, key_type=key_type)
+            store.save(creds, name="ca")
+            creds.set_store(store)
+        return creds
+
+    @staticmethod
+    def create_credentials(spec: CertificateSpec, issuer: Credentials, key_type: Any,
+                           valid_from: timedelta = timedelta(days=-1),
+                           valid_to: timedelta = timedelta(days=89),
+                           ) -> Credentials:
+        """Create a certificate signed by this CA for the given domains.
+        :returns: the certificate and private key PEM file paths
+        """
+        if spec.domains and len(spec.domains):
+            creds = MDTestCA._make_server_credentials(name=spec.name, domains=spec.domains,
+                                                      issuer=issuer, valid_from=valid_from,
+                                                      valid_to=valid_to, key_type=key_type)
+        elif spec.client:
+            creds = MDTestCA._make_client_credentials(name=spec.name, issuer=issuer,
+                                                      email=spec.email, valid_from=valid_from,
+                                                      valid_to=valid_to, key_type=key_type)
+        elif spec.name:
+            creds = MDTestCA._make_ca_credentials(name=spec.name, issuer=issuer,
+                                                  valid_from=valid_from, valid_to=valid_to,
+                                                  key_type=key_type)
+        else:
+            raise Exception(f"unrecognized certificate specification: {spec}")
+        return creds
+
+    @staticmethod
+    def _make_x509_name(org_name: str = None, common_name: str = None, parent: x509.Name = None) -> x509.Name:
+        name_pieces = []
+        if org_name:
+            oid = NameOID.ORGANIZATIONAL_UNIT_NAME if parent else NameOID.ORGANIZATION_NAME
+            name_pieces.append(x509.NameAttribute(oid, org_name))
+        elif common_name:
+            name_pieces.append(x509.NameAttribute(NameOID.COMMON_NAME, common_name))
+        if parent:
+            name_pieces.extend([rdn for rdn in parent])
+        return x509.Name(name_pieces)
+
+    @staticmethod
+    def _make_csr(
+            subject: x509.Name,
+            pkey: Any,
+            issuer_subject: Optional[Credentials],
+            valid_from_delta: timedelta = None,
+            valid_until_delta: timedelta = None
+    ):
+        pubkey = pkey.public_key()
+        issuer_subject = issuer_subject if issuer_subject is not None else subject
+
+        valid_from = datetime.now()
+        if valid_until_delta is not None:
+            valid_from += valid_from_delta
+        valid_until = datetime.now()
+        if valid_until_delta is not None:
+            valid_until += valid_until_delta
+
+        return (
+            x509.CertificateBuilder()
+            .subject_name(subject)
+            .issuer_name(issuer_subject)
+            .public_key(pubkey)
+            .not_valid_before(valid_from)
+            .not_valid_after(valid_until)
+            .serial_number(x509.random_serial_number())
+            .add_extension(
+                x509.SubjectKeyIdentifier.from_public_key(pubkey),
+                critical=False,
+            )
+        )
+
+    @staticmethod
+    def _add_ca_usages(csr: Any) -> Any:
+        return csr.add_extension(
+            x509.BasicConstraints(ca=True, path_length=9),
+            critical=True,
+        ).add_extension(
+            x509.KeyUsage(
+                digital_signature=True,
+                content_commitment=False,
+                key_encipherment=False,
+                data_encipherment=False,
+                key_agreement=False,
+                key_cert_sign=True,
+                crl_sign=True,
+                encipher_only=False,
+                decipher_only=False),
+            critical=True
+        ).add_extension(
+            x509.ExtendedKeyUsage([
+                ExtendedKeyUsageOID.CLIENT_AUTH,
+                ExtendedKeyUsageOID.SERVER_AUTH,
+                ExtendedKeyUsageOID.CODE_SIGNING,
+            ]),
+            critical=True
+        )
+
+    @staticmethod
+    def _add_leaf_usages(csr: Any, domains: List[str], issuer: Credentials) -> Any:
+        return csr.add_extension(
+            x509.BasicConstraints(ca=False, path_length=None),
+            critical=True,
+        ).add_extension(
+            x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
+                issuer.certificate.extensions.get_extension_for_class(
+                    x509.SubjectKeyIdentifier).value),
+            critical=False
+        ).add_extension(
+            x509.SubjectAlternativeName([x509.DNSName(domain) for domain in domains]),
+            critical=True,
+        ).add_extension(
+            x509.ExtendedKeyUsage([
+                ExtendedKeyUsageOID.SERVER_AUTH,
+            ]),
+            critical=True
+        )
+
+    @staticmethod
+    def _add_client_usages(csr: Any, issuer: Credentials, rfc82name: str = None) -> Any:
+        cert = csr.add_extension(
+            x509.BasicConstraints(ca=False, path_length=None),
+            critical=True,
+        ).add_extension(
+            x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
+                issuer.certificate.extensions.get_extension_for_class(
+                    x509.SubjectKeyIdentifier).value),
+            critical=False
+        )
+        if rfc82name:
+            cert.add_extension(
+                x509.SubjectAlternativeName([x509.RFC822Name(rfc82name)]),
+                critical=True,
+            )
+        cert.add_extension(
+            x509.ExtendedKeyUsage([
+                ExtendedKeyUsageOID.CLIENT_AUTH,
+            ]),
+            critical=True
+        )
+        return cert
+
+    @staticmethod
+    def _make_ca_credentials(name, key_type: Any,
+                             issuer: Credentials = None,
+                             valid_from: timedelta = timedelta(days=-1),
+                             valid_to: timedelta = timedelta(days=89),
+                             ) -> Credentials:
+        pkey = _private_key(key_type=key_type)
+        if issuer is not None:
+            issuer_subject = issuer.certificate.subject
+            issuer_key = issuer.private_key
+        else:
+            issuer_subject = None
+            issuer_key = pkey
+        subject = MDTestCA._make_x509_name(org_name=name, parent=issuer.subject if issuer else None)
+        csr = MDTestCA._make_csr(subject=subject,
+                                 issuer_subject=issuer_subject, pkey=pkey,
+                                 valid_from_delta=valid_from, valid_until_delta=valid_to)
+        csr = MDTestCA._add_ca_usages(csr)
+        cert = csr.sign(private_key=issuer_key,
+                        algorithm=hashes.SHA256(),
+                        backend=default_backend())
+        return Credentials(name=name, cert=cert, pkey=pkey)
+
+    @staticmethod
+    def _make_server_credentials(name: str, domains: List[str], issuer: Credentials,
+                                 key_type: Any,
+                                 valid_from: timedelta = timedelta(days=-1),
+                                 valid_to: timedelta = timedelta(days=89),
+                                 ) -> Credentials:
+        name = name
+        pkey = _private_key(key_type=key_type)
+        subject = MDTestCA._make_x509_name(common_name=name, parent=issuer.subject)
+        csr = MDTestCA._make_csr(subject=subject,
+                                 issuer_subject=issuer.certificate.subject, pkey=pkey,
+                                 valid_from_delta=valid_from, valid_until_delta=valid_to)
+        csr = MDTestCA._add_leaf_usages(csr, domains=domains, issuer=issuer)
+        cert = csr.sign(private_key=issuer.private_key,
+                        algorithm=hashes.SHA256(),
+                        backend=default_backend())
+        return Credentials(name=name, cert=cert, pkey=pkey)
+
+    @staticmethod
+    def _make_client_credentials(name: str,
+                                 issuer: Credentials, email: Optional[str],
+                                 key_type: Any,
+                                 valid_from: timedelta = timedelta(days=-1),
+                                 valid_to: timedelta = timedelta(days=89),
+                                 ) -> Credentials:
+        pkey = _private_key(key_type=key_type)
+        subject = MDTestCA._make_x509_name(common_name=name, parent=issuer.subject)
+        csr = MDTestCA._make_csr(subject=subject,
+                                 issuer_subject=issuer.certificate.subject, pkey=pkey,
+                                 valid_from_delta=valid_from, valid_until_delta=valid_to)
+        csr = MDTestCA._add_client_usages(csr, issuer=issuer, rfc82name=email)
+        cert = csr.sign(private_key=issuer.private_key,
+                        algorithm=hashes.SHA256(),
+                        backend=default_backend())
+        return Credentials(name=name, cert=cert, pkey=pkey)

Propchange: httpd/httpd/branches/2.4.x/test/modules/md/md_certs.py
------------------------------------------------------------------------------
    svn:executable = *

Added: httpd/httpd/branches/2.4.x/test/modules/md/md_conf.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/md/md_conf.py?rev=1895947&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/md/md_conf.py (added)
+++ httpd/httpd/branches/2.4.x/test/modules/md/md_conf.py Tue Dec 14 11:33:27 2021
@@ -0,0 +1,81 @@
+from .md_env import MDTestEnv
+from pyhttpd.conf import HttpdConf
+
+
+class MDConf(HttpdConf):
+
+    def __init__(self, env: MDTestEnv, text=None, std_ports=True,
+                 local_ca=True, std_vhosts=True, proxy=False,
+                 admin=None):
+        super().__init__(env=env)
+
+        if admin is None:
+            admin = f"admin@{env.http_tld}"
+        if len(admin.strip()):
+            self.add_admin(admin)
+
+        if local_ca:
+            self.add([
+                f"MDCertificateAuthority {env.acme_url}",
+                f"MDCertificateAgreement accepted",
+                f"MDCACertificateFile {env.server_dir}/acme-ca.pem",
+                "",
+                ])
+        if std_ports:
+            self.add(f"MDPortMap 80:{env.http_port} 443:{env.https_port}")
+            if env.ssl_module == "tls":
+                self.add(f"TLSListen {env.https_port}")
+        self.add([
+            "<Location /server-status>",
+            "    SetHandler server-status",
+            "</Location>",
+            "<Location /md-status>",
+            "    SetHandler md-status",
+            "</Location>",
+        ])
+        if std_vhosts:
+            self.add_vhost_test1()
+        if proxy:
+            self.add([
+                f"Listen {self.env.proxy_port}",
+                f"<VirtualHost *:{self.env.proxy_port}>",
+                "    ProxyRequests On",
+                "    ProxyVia On",
+                "    # be totally open",
+                "    AllowCONNECT 0-56535",
+                "    <Proxy *>",
+                "       # No require or other restrictions, this is just a test server",
+                "    </Proxy>",
+                "</VirtualHost>",
+            ])
+        if text is not None:
+            self.add(text)
+
+    def add_drive_mode(self, mode):
+        self.add("MDRenewMode \"%s\"\n" % mode)
+
+    def add_renew_window(self, window):
+        self.add("MDRenewWindow %s\n" % window)
+
+    def add_private_key(self, key_type, key_params):
+        self.add("MDPrivateKeys %s %s\n" % (key_type, " ".join(map(lambda p: str(p), key_params))))
+
+    def add_admin(self, email):
+        self.add(f"ServerAdmin mailto:{email}")
+
+    def add_md(self, domains):
+        dlist = " ".join(domains)    # without quotes
+        self.add(f"MDomain {dlist}\n")
+
+    def start_md(self, domains):
+        dlist = " ".join([f"\"{d}\"" for d in domains])  # with quotes, #257
+        self.add(f"<MDomain {dlist}>\n")
+        
+    def end_md(self):
+        self.add("</MDomain>\n")
+
+    def start_md2(self, domains):
+        self.add("<MDomainSet %s>\n" % " ".join(domains))
+
+    def end_md2(self):
+        self.add("</MDomainSet>\n")

Propchange: httpd/httpd/branches/2.4.x/test/modules/md/md_conf.py
------------------------------------------------------------------------------
    svn:executable = *