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 2024/09/17 11:38:19 UTC

svn commit: r1920747 - in /httpd/httpd/trunk: changes-entries/ modules/md/ test/modules/md/ test/pyhttpd/

Author: icing
Date: Tue Sep 17 11:38:19 2024
New Revision: 1920747

URL: http://svn.apache.org/viewvc?rev=1920747&view=rev
Log:
  *) mod_md: update to version 2.4.28
     - When the server starts, it looks for new, staged certificates to
       activate. If the staged set of files in 'md/staging/<domain>' is messed
       up, this could prevent further renewals to happen. Now, when the staging
       set is present, but could not be activated due to an error, purge the
       whole directory. [icing]
     - Fix certificate retrieval on ACME renewal to not require a 'Location:'
       header returned by the ACME CA. This was the way it was done in ACME
       before it became an IETF standard. Let's Encrypt still supports this,
       but other CAs do not. [icing]
     - Restore compatibility with OpenSSL < 1.1. [ylavic]


Added:
    httpd/httpd/trunk/changes-entries/md_v2.4.28.txt
Modified:
    httpd/httpd/trunk/modules/md/md_acme_drive.c
    httpd/httpd/trunk/modules/md/md_reg.c
    httpd/httpd/trunk/modules/md/md_version.h
    httpd/httpd/trunk/test/modules/md/conftest.py
    httpd/httpd/trunk/test/modules/md/md_cert_util.py
    httpd/httpd/trunk/test/modules/md/md_env.py
    httpd/httpd/trunk/test/modules/md/test_502_acmev2_drive.py
    httpd/httpd/trunk/test/modules/md/test_702_auto.py
    httpd/httpd/trunk/test/modules/md/test_730_static.py
    httpd/httpd/trunk/test/modules/md/test_741_setup_errors.py
    httpd/httpd/trunk/test/modules/md/test_801_stapling.py
    httpd/httpd/trunk/test/modules/md/test_901_message.py
    httpd/httpd/trunk/test/modules/md/test_920_status.py
    httpd/httpd/trunk/test/pyhttpd/certs.py

Added: httpd/httpd/trunk/changes-entries/md_v2.4.28.txt
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/changes-entries/md_v2.4.28.txt?rev=1920747&view=auto
==============================================================================
--- httpd/httpd/trunk/changes-entries/md_v2.4.28.txt (added)
+++ httpd/httpd/trunk/changes-entries/md_v2.4.28.txt Tue Sep 17 11:38:19 2024
@@ -0,0 +1,11 @@
+  *) mod_md: update to version 2.4.28
+     - When the server starts, it looks for new, staged certificates to
+       activate. If the staged set of files in 'md/staging/<domain>' is messed
+       up, this could prevent further renewals to happen. Now, when the staging
+       set is present, but could not be activated due to an error, purge the
+       whole directory. [icing]
+     - Fix certificate retrieval on ACME renewal to not require a 'Location:'
+       header returned by the ACME CA. This was the way it was done in ACME
+       before it became an IETF standard. Let's Encrypt still supports this,
+       but other CAs do not. [icing]
+     - Restore compatibility with OpenSSL < 1.1. [ylavic]

Modified: httpd/httpd/trunk/modules/md/md_acme_drive.c
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/modules/md/md_acme_drive.c?rev=1920747&r1=1920746&r2=1920747&view=diff
==============================================================================
--- httpd/httpd/trunk/modules/md/md_acme_drive.c (original)
+++ httpd/httpd/trunk/modules/md/md_acme_drive.c Tue Sep 17 11:38:19 2024
@@ -305,11 +305,11 @@ static apr_status_t csr_req(md_acme_t *a
     
     (void)acme;
     location = apr_table_get(res->headers, "location");
-    if (!location) {
-        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, d->p, 
-                      "cert created without giving its location header");
-        return APR_EINVAL;
-    }
+    if (!location)
+      return rv;
+
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p,
+                  "cert created with location header (old ACMEv1 style)");
     ad->order->certificate = apr_pstrdup(d->p, location);
     if (APR_SUCCESS != (rv = md_acme_order_save(d->store, d->p, MD_SG_STAGING, 
                                                 d->md->name, ad->order, 0))) { 

Modified: httpd/httpd/trunk/modules/md/md_reg.c
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/modules/md/md_reg.c?rev=1920747&r1=1920746&r2=1920747&view=diff
==============================================================================
--- httpd/httpd/trunk/modules/md/md_reg.c (original)
+++ httpd/httpd/trunk/modules/md/md_reg.c Tue Sep 17 11:38:19 2024
@@ -1194,7 +1194,7 @@ static apr_status_t run_load_staging(voi
     result =  va_arg(ap, md_result_t*);
     
     if (APR_STATUS_IS_ENOENT(rv = md_load(reg->store, MD_SG_STAGING, md->name, NULL, ptemp))) {
-        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, ptemp, "%s: nothing staged", md->name);
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "%s: nothing staged", md->name);
         goto out;
     }
     
@@ -1259,7 +1259,9 @@ apr_status_t md_reg_load_stagings(md_reg
         }
         else if (!APR_STATUS_IS_ENOENT(rv)) {
             md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, APLOGNO(10069)
-                          "%s: error loading staged set", md->name);
+                          "%s: error loading staged set, purging it", md->name);
+            md_store_purge(reg->store, p, MD_SG_STAGING, md->name);
+            md_store_purge(reg->store, p, MD_SG_CHALLENGES, md->name);
         }
     }
 

Modified: httpd/httpd/trunk/modules/md/md_version.h
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/modules/md/md_version.h?rev=1920747&r1=1920746&r2=1920747&view=diff
==============================================================================
--- httpd/httpd/trunk/modules/md/md_version.h (original)
+++ httpd/httpd/trunk/modules/md/md_version.h Tue Sep 17 11:38:19 2024
@@ -27,7 +27,7 @@
  * @macro
  * Version number of the md module as c string
  */
-#define MOD_MD_VERSION "2.4.26"
+#define MOD_MD_VERSION "2.4.28"
 
 /**
  * @macro
@@ -35,7 +35,7 @@
  * release. This is a 24 bit number with 8 bits for major number, 8 bits
  * for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203.
  */
-#define MOD_MD_VERSION_NUM 0x02041a
+#define MOD_MD_VERSION_NUM 0x02041c
 
 #define MD_ACME_DEF_URL         "https://acme-v02.api.letsencrypt.org/directory"
 #define MD_TAILSCALE_DEF_URL    "file://localhost/var/run/tailscale/tailscaled.sock"

Modified: httpd/httpd/trunk/test/modules/md/conftest.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/conftest.py?rev=1920747&r1=1920746&r2=1920747&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/md/conftest.py (original)
+++ httpd/httpd/trunk/test/modules/md/conftest.py Tue Sep 17 11:38:19 2024
@@ -39,9 +39,7 @@ def env(pytestconfig) -> MDTestEnv:
 @pytest.fixture(autouse=True, scope="package")
 def _md_package_scope(env):
     env.httpd_error_log.add_ignored_lognos([
-        "AH10085",   # There are no SSL certificates configured and no other module contributed any
-        "AH10045",   # No VirtualHost matches Managed Domain
-        "AH10105",   # MDomain does not match any VirtualHost with 'SSLEngine on'
+        "AH10085"   # There are no SSL certificates configured and no other module contributed any
     ])
 
 
@@ -59,7 +57,3 @@ def acme(env):
     if acme_server is not None:
         acme_server.stop()
 
-@pytest.fixture(autouse=True, scope="package")
-def _stop_package_scope(env):
-    yield
-    assert env.apache_stop() == 0

Modified: httpd/httpd/trunk/test/modules/md/md_cert_util.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/md_cert_util.py?rev=1920747&r1=1920746&r2=1920747&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/md/md_cert_util.py (original)
+++ httpd/httpd/trunk/test/modules/md/md_cert_util.py Tue Sep 17 11:38:19 2024
@@ -1,6 +1,5 @@
 import logging
 import re
-import os
 import socket
 import OpenSSL
 import time
@@ -12,6 +11,7 @@ from datetime import timedelta
 from http.client import HTTPConnection
 from urllib.parse import urlparse
 
+from cryptography import x509
 
 SEC_PER_DAY = 24 * 60 * 60
 
@@ -24,45 +24,6 @@ class MDCertUtil(object):
     # 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:
@@ -138,17 +99,26 @@ class MDCertUtil(object):
         # 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):
+    @staticmethod
+    def _get_serial(cert) -> int:
+        if isinstance(cert, x509.Certificate):
+            return cert.serial_number
+        if isinstance(cert, MDCertUtil):
+            return cert.get_serial_number()
+        elif isinstance(cert, OpenSSL.crypto.X509):
+            return cert.get_serial_number()
+        elif isinstance(cert, 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
+            return int(cert, 16)
+        elif isinstance(cert, int):
+            return cert
+        return 0
+
+    def get_serial_number(self):
+        return self._get_serial(self.cert)
+
+    def same_serial_as(self, other):
+        return self._get_serial(self.cert) == self._get_serial(other)
 
     def get_not_before(self):
         tsp = self.cert.get_notBefore()

Modified: httpd/httpd/trunk/test/modules/md/md_env.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/md_env.py?rev=1920747&r1=1920746&r2=1920747&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/md/md_env.py (original)
+++ httpd/httpd/trunk/test/modules/md/md_env.py Tue Sep 17 11:38:19 2024
@@ -12,9 +12,9 @@ import subprocess
 import time
 
 from datetime import datetime, timedelta
-from typing import Dict, Optional
+from typing import Dict, Optional, Any
 
-from pyhttpd.certs import CertificateSpec
+from pyhttpd.certs import CertificateSpec, Credentials, HttpdTestCA
 from .md_cert_util import MDCertUtil
 from pyhttpd.env import HttpdTestSetup, HttpdTestEnv
 from pyhttpd.result import ExecResult
@@ -73,10 +73,10 @@ class MDTestEnv(HttpdTestEnv):
 
     @classmethod
     def has_acme_eab(cls):
-        # Pebble v2.5.0 and v2.5.1 do not support HS256 for EAB, which
-        # is the only thing mod_md supports.
-        # Should work for pebble until v2.4.0 and v2.5.2+.
-        # Reference: https://github.com/letsencrypt/pebble/issues/455
+        # Pebble, in v2.5.0 no longer supported HS256 for EAB, which
+        # is the only thing mod_md supports. Issue opened at pebble:
+        # https://github.com/letsencrypt/pebble/issues/455
+        # is fixed in v2.6.0
         return cls.get_acme_server() == 'pebble'
 
     @classmethod
@@ -611,8 +611,13 @@ class MDTestEnv(HttpdTestEnv):
             time.sleep(0.1)
         raise TimeoutError(f"ocsp respopnse not available: {domain}")
 
-    def create_self_signed_cert(self, name_list, valid_days, serial=1000, path=None):
-        dirpath = path
-        if not path:
-            dirpath = os.path.join(self.store_domains(), name_list[0])
-        return MDCertUtil.create_self_signed_cert(dirpath, name_list, valid_days, serial)
+    def create_self_signed_cert(self, spec: CertificateSpec,
+                                valid_from: timedelta = timedelta(days=-1),
+                                valid_to: timedelta = timedelta(days=89),
+                                serial: Optional[int] = None) -> Credentials:
+        key_type = spec.key_type if spec.key_type else 'rsa4096'
+        return HttpdTestCA.create_credentials(spec=spec, issuer=None,
+                                              key_type=key_type,
+                                              valid_from=valid_from,
+                                              valid_to=valid_to,
+                                              serial=serial)

Modified: 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=1920747&r1=1920746&r2=1920747&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_502_acmev2_drive.py (original)
+++ httpd/httpd/trunk/test/modules/md/test_502_acmev2_drive.py Tue Sep 17 11:38:19 2024
@@ -4,11 +4,12 @@ import base64
 import json
 import os.path
 import re
-import time
+from datetime import timedelta
 
 import pytest
+from pyhttpd.certs import CertificateSpec
 
-from .md_conf import MDConf, MDConf
+from .md_conf import MDConf
 from .md_cert_util import MDCertUtil
 from .md_env import MDTestEnv
 
@@ -430,9 +431,12 @@ class TestDrivev2:
         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)
+            creds = env.create_self_signed_cert(CertificateSpec(domains=[name]),
+                                                valid_from=timedelta(days=tc["valid"]["notBefore"]),
+                                                valid_to=timedelta(days=tc["valid"]["notAfter"]))
+            assert creds.certificate.serial_number != cert1.get_serial_number()
+            # copy it over, assess status again
+            creds.save_cert_pem(env.store_domain_file(name, 'pubcert.pem'))
             md = env.a2md(["list", name]).json['output'][0]
             assert md["renew"] == tc["renew"], \
                 "Expected renew == {} indicator in {}, test case {}".format(tc["renew"], md, tc)

Modified: httpd/httpd/trunk/test/modules/md/test_702_auto.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_702_auto.py?rev=1920747&r1=1920746&r2=1920747&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_702_auto.py (original)
+++ httpd/httpd/trunk/test/modules/md/test_702_auto.py Tue Sep 17 11:38:19 2024
@@ -1,9 +1,9 @@
 import os
-import time
+from datetime import timedelta
 
 import pytest
+from pyhttpd.certs import CertificateSpec
 
-from pyhttpd.conf import HttpdConf
 from pyhttpd.env import HttpdTestEnv
 from .md_cert_util import MDCertUtil
 from .md_env import MDTestEnv
@@ -320,18 +320,22 @@ class TestAutov2:
         assert cert1.same_serial_as(stat['rsa']['serial'])
         #
         # create self-signed cert, with critical remaining valid duration -> drive again
-        env.create_self_signed_cert([domain], {"notBefore": -120, "notAfter": 2}, serial=7029)
-        cert3 = MDCertUtil(env.store_domain_file(domain, 'pubcert.pem'))
-        assert cert3.same_serial_as('1B75')
+        creds = env.create_self_signed_cert(CertificateSpec(domains=[domain]),
+                                            valid_from=timedelta(days=-120),
+                                            valid_to=timedelta(days=2),
+                                            serial=7029)
+        creds.save_cert_pem(env.store_domain_file(domain, 'pubcert.pem'))
+        creds.save_pkey_pem(env.store_domain_file(domain, 'privkey.pem'))
+        assert creds.certificate.serial_number == 7029
         assert env.apache_restart() == 0
         stat = env.get_certificate_status(domain)
-        assert cert3.same_serial_as(stat['rsa']['serial'])
+        assert creds.certificate.serial_number == int(stat['rsa']['serial'], 16)
         #
         # cert should renew and be different afterwards
         assert env.await_completion([domain], must_renew=True)
         stat = env.get_certificate_status(domain)
-        assert not cert3.same_serial_as(stat['rsa']['serial'])
-        
+        creds.certificate.serial_number != int(stat['rsa']['serial'], 16)
+
     # test case: drive with an unsupported challenge due to port availability 
     def test_md_702_010(self, env):
         domain = self.test_domain
@@ -543,6 +547,40 @@ class TestAutov2:
         assert name2 in cert1b.get_san_list()
         assert not cert1.same_serial_as(cert1b)
 
+    # test case: one MD on a vhost with ServerAlias. Renew.
+    # Exchange ServerName and ServerAlias. Is the rename detected?
+    # See: https://github.com/icing/mod_md/issues/338
+    def test_md_702_033(self, env):
+        domain = self.test_domain
+        name_x = "test-x." + domain
+        name_a = "test-a." + domain
+        domains1 = [name_x, name_a]
+        #
+        # generate 1 MD and 2 vhosts
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_md(domains=[name_x])
+        conf.add_vhost(domains=domains1)
+        conf.install()
+        #
+        # restart (-> drive), check that MD was synched and completes
+        assert env.apache_restart() == 0
+        env.check_md(domains1)
+        assert env.await_completion([name_x])
+        env.check_md_complete(name_x)
+        cert_x = env.get_cert(name_x)
+        #
+        # reverse ServerName and ServerAlias
+        domains2 = [name_a, name_x]
+        conf = MDConf(env, admin="admin@" + domain)
+        conf.add_md(domains=[name_a])
+        conf.add_vhost(domains=domains2)
+        conf.install()
+        # restart, check that host still works and kept the cert
+        assert env.apache_restart() == 0
+        status = env.get_certificate_status(name_a)
+        assert cert_x.same_serial_as(status['rsa']['serial'])
+
+
     # test case: test "tls-alpn-01" challenge handling
     def test_md_702_040(self, env):
         domain = self.test_domain

Modified: httpd/httpd/trunk/test/modules/md/test_730_static.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_730_static.py?rev=1920747&r1=1920746&r2=1920747&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_730_static.py (original)
+++ httpd/httpd/trunk/test/modules/md/test_730_static.py Tue Sep 17 11:38:19 2024
@@ -1,6 +1,8 @@
 import os
+from datetime import timedelta
 
 import pytest
+from pyhttpd.certs import CertificateSpec
 
 from .md_conf import MDConf
 from .md_env import MDTestEnv
@@ -30,12 +32,14 @@ class TestStatic:
         domains = [domain, 'www.%s' % domain]
         testpath = os.path.join(env.gen_dir, 'test_920_001')
         # cert that is only 10 more days valid
-        env.create_self_signed_cert(domains, {"notBefore": -80, "notAfter": 10},
-                                    serial=730001, path=testpath)
+        creds = env.create_self_signed_cert(CertificateSpec(domains=domains),
+                                            valid_from=timedelta(days=-80),
+                                            valid_to=timedelta(days=10),
+                                            serial=730001)
         cert_file = os.path.join(testpath, 'pubcert.pem')
         pkey_file = os.path.join(testpath, 'privkey.pem')
-        assert os.path.exists(cert_file)
-        assert os.path.exists(pkey_file)
+        creds.save_cert_pem(cert_file)
+        creds.save_pkey_pem(pkey_file)
         conf = MDConf(env)
         conf.start_md(domains)
         conf.add(f"MDCertificateFile {cert_file}")
@@ -60,12 +64,14 @@ class TestStatic:
         domains = [domain, 'www.%s' % domain]
         testpath = os.path.join(env.gen_dir, 'test_920_001')
         # cert that is only 10 more days valid
-        env.create_self_signed_cert(domains, {"notBefore": -80, "notAfter": 10},
-                                    serial=730001, path=testpath)
+        creds = env.create_self_signed_cert(CertificateSpec(domains=domains),
+                                            valid_from=timedelta(days=-80),
+                                            valid_to=timedelta(days=10),
+                                            serial=730001)
         cert_file = os.path.join(testpath, 'pubcert.pem')
         pkey_file = os.path.join(testpath, 'privkey.pem')
-        assert os.path.exists(cert_file)
-        assert os.path.exists(pkey_file)
+        creds.save_cert_pem(cert_file)
+        creds.save_pkey_pem(pkey_file)
         conf = MDConf(env)
         conf.start_md(domains)
         conf.add(f"MDPrivateKeys secp384r1 rsa3072")
@@ -93,13 +99,14 @@ class TestStatic:
         domains = [domain, 'www.%s' % domain]
         testpath = os.path.join(env.gen_dir, 'test_920_001')
         # cert that is only 10 more days valid
-        env.create_self_signed_cert(domains, {"notBefore": -80, "notAfter": 10},
-                                    serial=730001, path=testpath)
+        creds = env.create_self_signed_cert(CertificateSpec(domains=domains),
+                                            valid_from=timedelta(days=-80),
+                                            valid_to=timedelta(days=10),
+                                            serial=730001)
         cert_file = os.path.join(testpath, 'pubcert.pem')
         pkey_file = os.path.join(testpath, 'privkey.pem')
-        assert os.path.exists(cert_file)
-        assert os.path.exists(pkey_file)
-        
+        creds.save_cert_pem(cert_file)
+        creds.save_pkey_pem(pkey_file)
         conf = MDConf(env)
         conf.start_md(domains)
         conf.add(f"MDCertificateFile {cert_file}")

Modified: httpd/httpd/trunk/test/modules/md/test_741_setup_errors.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_741_setup_errors.py?rev=1920747&r1=1920746&r2=1920747&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_741_setup_errors.py (original)
+++ httpd/httpd/trunk/test/modules/md/test_741_setup_errors.py Tue Sep 17 11:38:19 2024
@@ -56,3 +56,29 @@ class TestSetupErrors:
                 r'.*CA considers answer to challenge invalid.*'
             ]
         )
+
+    # mess up the produced staging area before reload
+    def test_md_741_002(self, env):
+        domain = self.test_domain
+        domains = [domain]
+        conf = MDConf(env)
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+        assert env.apache_restart() == 0
+        env.check_md(domains)
+        assert env.await_completion([domain], restart=False)
+        staged_md_path = env.store_staged_file(domain, 'md.json')
+        with open(staged_md_path, 'w') as fd:
+            fd.write('garbage\n')
+        assert env.apache_restart() == 0
+        assert env.await_completion([domain])
+        env.check_md_complete(domain)
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10069"   # failed to load JSON file
+            ],
+            matches = [
+                r'.*failed to load JSON file.*',
+            ]
+        )

Modified: httpd/httpd/trunk/test/modules/md/test_801_stapling.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_801_stapling.py?rev=1920747&r1=1920746&r2=1920747&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_801_stapling.py (original)
+++ httpd/httpd/trunk/test/modules/md/test_801_stapling.py Tue Sep 17 11:38:19 2024
@@ -2,7 +2,9 @@
 
 import os
 import time
+from datetime import timedelta
 import pytest
+from pyhttpd.certs import CertificateSpec
 
 from .md_conf import MDConf
 from .md_env import MDTestEnv
@@ -334,12 +336,14 @@ class TestStapling:
         domains = [md]
         testpath = os.path.join(env.gen_dir, 'test_801_009')
         # cert that is 30 more days valid
-        env.create_self_signed_cert(domains, {"notBefore": -60, "notAfter": 30},
-                                        serial=801009, path=testpath)
+        creds = env.create_self_signed_cert(CertificateSpec(domains=domains),
+                                            valid_from=timedelta(days=-60),
+                                            valid_to=timedelta(days=30),
+                                            serial=801009)
         cert_file = os.path.join(testpath, 'pubcert.pem')
         pkey_file = os.path.join(testpath, 'privkey.pem')
-        assert os.path.exists(cert_file)
-        assert os.path.exists(pkey_file)
+        creds.save_cert_pem(cert_file)
+        creds.save_pkey_pem(pkey_file)
         conf = MDConf(env)
         conf.start_md(domains)
         conf.add("MDCertificateFile %s" % cert_file)

Modified: httpd/httpd/trunk/test/modules/md/test_901_message.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_901_message.py?rev=1920747&r1=1920746&r2=1920747&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_901_message.py (original)
+++ httpd/httpd/trunk/test/modules/md/test_901_message.py Tue Sep 17 11:38:19 2024
@@ -3,9 +3,11 @@
 import json
 import os
 import time
+from datetime import timedelta
 import pytest
+from pyhttpd.certs import CertificateSpec
 
-from .md_conf import MDConf, MDConf
+from .md_conf import MDConf
 from .md_env import MDTestEnv
 
 
@@ -155,13 +157,15 @@ class TestMessage:
         domain = self.test_domain
         domains = [domain, 'www.%s' % domain]
         testpath = os.path.join(env.gen_dir, 'test_901_010')
-        # cert that is only 10 more days valid
-        env.create_self_signed_cert(domains, {"notBefore": -70, "notAfter": 20},
-                                    serial=901010, path=testpath)
+        # cert that is only 20 more days valid
+        creds = env.create_self_signed_cert(CertificateSpec(domains=domains),
+                                            valid_from=timedelta(days=-70),
+                                            valid_to=timedelta(days=20),
+                                            serial=901010)
         cert_file = os.path.join(testpath, 'pubcert.pem')
         pkey_file = os.path.join(testpath, 'privkey.pem')
-        assert os.path.exists(cert_file)
-        assert os.path.exists(pkey_file)
+        creds.save_cert_pem(cert_file)
+        creds.save_pkey_pem(pkey_file)
         conf = MDConf(env)
         conf.add(f"MDMessageCmd {self.mcmd} {self.mlog}")
         conf.start_md(domains)
@@ -178,13 +182,15 @@ class TestMessage:
         domain = self.test_domain
         domains = [domain, f'www.{domain}']
         testpath = os.path.join(env.gen_dir, 'test_901_011')
-        # cert that is only 10 more days valid
-        env.create_self_signed_cert(domains, {"notBefore": -85, "notAfter": 5},
-                                    serial=901011, path=testpath)
+        # cert that is only 5 more days valid
+        creds = env.create_self_signed_cert(CertificateSpec(domains=domains),
+                                            valid_from=timedelta(days=-85),
+                                            valid_to=timedelta(days=5),
+                                            serial=901010)
         cert_file = os.path.join(testpath, 'pubcert.pem')
         pkey_file = os.path.join(testpath, 'privkey.pem')
-        assert os.path.exists(cert_file)
-        assert os.path.exists(pkey_file)
+        creds.save_cert_pem(cert_file)
+        creds.save_pkey_pem(pkey_file)
         conf = MDConf(env)
         conf.add(f"MDMessageCmd {self.mcmd} {self.mlog}")
         conf.start_md(domains)

Modified: httpd/httpd/trunk/test/modules/md/test_920_status.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/md/test_920_status.py?rev=1920747&r1=1920746&r2=1920747&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/md/test_920_status.py (original)
+++ httpd/httpd/trunk/test/modules/md/test_920_status.py Tue Sep 17 11:38:19 2024
@@ -2,9 +2,10 @@
 
 import os
 import re
-import time
+from datetime import timedelta
 
 import pytest
+from pyhttpd.certs import CertificateSpec
 
 from .md_conf import MDConf
 from shutil import copyfile
@@ -165,13 +166,15 @@ Protocols h2 http/1.1 acme-tls/1
         domain = self.test_domain
         domains = [domain, 'www.%s' % domain]
         testpath = os.path.join(env.gen_dir, 'test_920_011')
-        # cert that is only 10 more days valid
-        env.create_self_signed_cert(domains, {"notBefore": -70, "notAfter": 20},
-                                    serial=920011, path=testpath)
+        # cert that is only 20 more days valid
+        creds = env.create_self_signed_cert(CertificateSpec(domains=domains),
+                                            valid_from=timedelta(days=-70),
+                                            valid_to=timedelta(days=20),
+                                            serial=920011)
         cert_file = os.path.join(testpath, 'pubcert.pem')
         pkey_file = os.path.join(testpath, 'privkey.pem')
-        assert os.path.exists(cert_file)
-        assert os.path.exists(pkey_file)
+        creds.save_cert_pem(cert_file)
+        creds.save_pkey_pem(pkey_file)
         conf = MDConf(env, std_vhosts=False, std_ports=False, text=f"""
         MDBaseServer on
         MDPortMap http:- https:{env.https_port}

Modified: httpd/httpd/trunk/test/pyhttpd/certs.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/pyhttpd/certs.py?rev=1920747&r1=1920746&r2=1920747&view=diff
==============================================================================
--- httpd/httpd/trunk/test/pyhttpd/certs.py (original)
+++ httpd/httpd/trunk/test/pyhttpd/certs.py Tue Sep 17 11:38:19 2024
@@ -181,6 +181,14 @@ class Credentials:
             creds.issue_certs(spec.sub_specs, chain=subchain)
         return creds
 
+    def save_cert_pem(self, fpath):
+        with open(fpath, "wb") as fd:
+            fd.write(self.cert_pem)
+
+    def save_pkey_pem(self, fpath):
+        with open(fpath, "wb") as fd:
+            fd.write(self.pkey_pem)
+
 
 class CertStore:
 
@@ -282,6 +290,7 @@ class HttpdTestCA:
     def create_credentials(spec: CertificateSpec, issuer: Credentials, key_type: Any,
                            valid_from: timedelta = timedelta(days=-1),
                            valid_to: timedelta = timedelta(days=89),
+                           serial: Optional[int] = None,
                            ) -> Credentials:
         """Create a certificate signed by this CA for the given domains.
         :returns: the certificate and private key PEM file paths
@@ -289,15 +298,18 @@ class HttpdTestCA:
         if spec.domains and len(spec.domains):
             creds = HttpdTestCA._make_server_credentials(name=spec.name, domains=spec.domains,
                                                          issuer=issuer, valid_from=valid_from,
-                                                         valid_to=valid_to, key_type=key_type)
+                                                         valid_to=valid_to, key_type=key_type,
+                                                         serial=serial)
         elif spec.client:
             creds = HttpdTestCA._make_client_credentials(name=spec.name, issuer=issuer,
                                                          email=spec.email, valid_from=valid_from,
-                                                         valid_to=valid_to, key_type=key_type)
+                                                         valid_to=valid_to, key_type=key_type,
+                                                         serial=serial)
         elif spec.name:
             creds = HttpdTestCA._make_ca_credentials(name=spec.name, issuer=issuer,
                                                      valid_from=valid_from, valid_to=valid_to,
-                                                     key_type=key_type)
+                                                     key_type=key_type,
+                                                     serial=serial)
         else:
             raise Exception(f"unrecognized certificate specification: {spec}")
         return creds
@@ -320,7 +332,8 @@ class HttpdTestCA:
             pkey: Any,
             issuer_subject: Optional[Credentials],
             valid_from_delta: timedelta = None,
-            valid_until_delta: timedelta = None
+            valid_until_delta: timedelta = None,
+            serial: Optional[int] = None
     ):
         pubkey = pkey.public_key()
         issuer_subject = issuer_subject if issuer_subject is not None else subject
@@ -331,7 +344,8 @@ class HttpdTestCA:
         valid_until = datetime.now()
         if valid_until_delta is not None:
             valid_until += valid_until_delta
-
+        if serial is None:
+            serial = x509.random_serial_number()
         return (
             x509.CertificateBuilder()
             .subject_name(subject)
@@ -339,7 +353,7 @@ class HttpdTestCA:
             .public_key(pubkey)
             .not_valid_before(valid_from)
             .not_valid_after(valid_until)
-            .serial_number(x509.random_serial_number())
+            .serial_number(serial)
             .add_extension(
                 x509.SubjectKeyIdentifier.from_public_key(pubkey),
                 critical=False,
@@ -374,23 +388,28 @@ class HttpdTestCA:
 
     @staticmethod
     def _add_leaf_usages(csr: Any, domains: List[str], issuer: Credentials) -> Any:
-        return csr.add_extension(
+        csr = 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(
+        )
+        if issuer is not None:
+            csr = csr.add_extension(
+                x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
+                    issuer.certificate.extensions.get_extension_for_class(
+                        x509.SubjectKeyIdentifier).value),
+                critical=False
+            )
+        csr = csr.add_extension(
             x509.SubjectAlternativeName([x509.DNSName(domain) for domain in domains]),
             critical=True,
-        ).add_extension(
+        )
+        csr = csr.add_extension(
             x509.ExtendedKeyUsage([
                 ExtendedKeyUsageOID.SERVER_AUTH,
             ]),
             critical=True
         )
+        return csr
 
     @staticmethod
     def _add_client_usages(csr: Any, issuer: Credentials, rfc82name: str = None) -> Any:
@@ -421,6 +440,7 @@ class HttpdTestCA:
                              issuer: Credentials = None,
                              valid_from: timedelta = timedelta(days=-1),
                              valid_to: timedelta = timedelta(days=89),
+                             serial: Optional[int] = None,
                              ) -> Credentials:
         pkey = _private_key(key_type=key_type)
         if issuer is not None:
@@ -432,7 +452,8 @@ class HttpdTestCA:
         subject = HttpdTestCA._make_x509_name(org_name=name, parent=issuer.subject if issuer else None)
         csr = HttpdTestCA._make_csr(subject=subject,
                                     issuer_subject=issuer_subject, pkey=pkey,
-                                    valid_from_delta=valid_from, valid_until_delta=valid_to)
+                                    valid_from_delta=valid_from, valid_until_delta=valid_to,
+                                    serial=serial)
         csr = HttpdTestCA._add_ca_usages(csr)
         cert = csr.sign(private_key=issuer_key,
                         algorithm=hashes.SHA256(),
@@ -444,15 +465,23 @@ class HttpdTestCA:
                                  key_type: Any,
                                  valid_from: timedelta = timedelta(days=-1),
                                  valid_to: timedelta = timedelta(days=89),
+                                 serial: Optional[int] = None,
                                  ) -> Credentials:
         name = name
         pkey = _private_key(key_type=key_type)
-        subject = HttpdTestCA._make_x509_name(common_name=name, parent=issuer.subject)
+        if issuer is not None:
+            issuer_subject = issuer.certificate.subject
+            issuer_key = issuer.private_key
+        else:
+            issuer_subject = None
+            issuer_key = pkey
+        subject = HttpdTestCA._make_x509_name(common_name=name, parent=issuer_subject)
         csr = HttpdTestCA._make_csr(subject=subject,
-                                    issuer_subject=issuer.certificate.subject, pkey=pkey,
-                                    valid_from_delta=valid_from, valid_until_delta=valid_to)
+                                    issuer_subject=issuer_subject, pkey=pkey,
+                                    valid_from_delta=valid_from, valid_until_delta=valid_to,
+                                    serial=serial)
         csr = HttpdTestCA._add_leaf_usages(csr, domains=domains, issuer=issuer)
-        cert = csr.sign(private_key=issuer.private_key,
+        cert = csr.sign(private_key=issuer_key,
                         algorithm=hashes.SHA256(),
                         backend=default_backend())
         return Credentials(name=name, cert=cert, pkey=pkey, issuer=issuer)
@@ -463,14 +492,22 @@ class HttpdTestCA:
                                  key_type: Any,
                                  valid_from: timedelta = timedelta(days=-1),
                                  valid_to: timedelta = timedelta(days=89),
+                                 serial: Optional[int] = None,
                                  ) -> Credentials:
         pkey = _private_key(key_type=key_type)
-        subject = HttpdTestCA._make_x509_name(common_name=name, parent=issuer.subject)
+        if issuer is not None:
+            issuer_subject = issuer.certificate.subject
+            issuer_key = issuer.private_key
+        else:
+            issuer_subject = None
+            issuer_key = pkey
+        subject = HttpdTestCA._make_x509_name(common_name=name, parent=issuer_subject)
         csr = HttpdTestCA._make_csr(subject=subject,
-                                    issuer_subject=issuer.certificate.subject, pkey=pkey,
-                                    valid_from_delta=valid_from, valid_until_delta=valid_to)
+                                    issuer_subject=issuer_subject, pkey=pkey,
+                                    valid_from_delta=valid_from, valid_until_delta=valid_to,
+                                    serial=serial)
         csr = HttpdTestCA._add_client_usages(csr, issuer=issuer, rfc82name=email)
-        cert = csr.sign(private_key=issuer.private_key,
+        cert = csr.sign(private_key=issuer_key,
                         algorithm=hashes.SHA256(),
                         backend=default_backend())
         return Credentials(name=name, cert=cert, pkey=pkey, issuer=issuer)