]> git.ipfire.org Git - thirdparty/apache/httpd.git/commitdiff
Merge /httpd/httpd/trunk:r1920747,1920751
authorStefan Eissing <icing@apache.org>
Tue, 17 Sep 2024 12:10:23 +0000 (12:10 +0000)
committerStefan Eissing <icing@apache.org>
Tue, 17 Sep 2024 12:10:23 +0000 (12:10 +0000)
  *) 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]

git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/branches/2.4.x@1920753 13f79535-47bb-0310-9956-ffa450edef68

15 files changed:
changes-entries/md_v2.4.28.txt [new file with mode: 0644]
modules/md/md_acme_drive.c
modules/md/md_reg.c
modules/md/md_version.h
test/modules/md/conftest.py
test/modules/md/md_cert_util.py
test/modules/md/md_env.py
test/modules/md/test_502_acmev2_drive.py
test/modules/md/test_702_auto.py
test/modules/md/test_730_static.py
test/modules/md/test_741_setup_errors.py
test/modules/md/test_801_stapling.py
test/modules/md/test_901_message.py
test/modules/md/test_920_status.py
test/pyhttpd/certs.py

diff --git a/changes-entries/md_v2.4.28.txt b/changes-entries/md_v2.4.28.txt
new file mode 100644 (file)
index 0000000..3eb2bc4
--- /dev/null
@@ -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]
index 4bb04f321c625c999a0cc8637527323baa51dd5e..0ec409c8637b2d20a4d6ec3a037979e8f280109e 100644 (file)
@@ -305,11 +305,11 @@ static apr_status_t csr_req(md_acme_t *acme, const md_http_response_t *res, void
     
     (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))) { 
index 6aa7d788769398a7ed500f16c7034600fe051354..dc49446ae452011dabbb528a0414f1110aff302e 100644 (file)
@@ -1194,7 +1194,7 @@ static apr_status_t run_load_staging(void *baton, apr_pool_t *p, apr_pool_t *pte
     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_t *reg, apr_array_header_t *mds,
         }
         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);
         }
     }
 
index cefbb8ded7287e3e6d5d7d152808d8fb093fc497..3e2914d6b6d249ec7f074c20975972b8cd063ce3 100644 (file)
@@ -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"
index a7b064b6a981b12ccb3f676e6a33a58703b28dfa..0118de5e1339705855a28f157082af4637b3dab5 100755 (executable)
@@ -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
index abcd36b938c37bda3e249c75b9889750297992ce..6cd034a02b5825efe884a709724e9adf79466dd4 100755 (executable)
@@ -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
 
@@ -23,45 +23,6 @@ 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)
@@ -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()
index 360086f97b3e8dec3d08f23d23fc2b3a507e2c09..acc8417b149ec4160e111396ce9f97ba21307e1d 100755 (executable)
@@ -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)
index eb754f25eff9fafea6bbe933b974fc24a1acaf31..b064647450e4dd66fdf3a0f7077c76a47ab966ba 100644 (file)
@@ -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)
index 04a9c7561aa95465c842a3a712ae30945cc7d0c7..90103e3aff7c8d3f5e9afeb9604def37978d27cb 100644 (file)
@@ -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
index 891ae620bb8e2d394add2bfd8d26a6d68a463a12..91a5f4445d2bc694bd9fe9ec6644fdd1f8072e23 100644 (file)
@@ -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
@@ -28,14 +30,17 @@ class TestStatic:
         # MD with static cert files, will not be driven
         domain = self.test_domain
         domains = [domain, 'www.%s' % domain]
-        testpath = os.path.join(env.gen_dir, 'test_920_001')
+        testpath = os.path.join(env.gen_dir, 'test_730_001')
+        env.mkpath(testpath)
         # 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}")
@@ -58,14 +63,17 @@ class TestStatic:
         # MD with static cert files, force driving
         domain = self.test_domain
         domains = [domain, 'www.%s' % domain]
-        testpath = os.path.join(env.gen_dir, 'test_920_001')
+        testpath = os.path.join(env.gen_dir, 'test_730_002')
+        env.mkpath(testpath)
         # 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")
@@ -91,15 +99,17 @@ class TestStatic:
         # just configuring one file will not work
         domain = self.test_domain
         domains = [domain, 'www.%s' % domain]
-        testpath = os.path.join(env.gen_dir, 'test_920_001')
+        testpath = os.path.join(env.gen_dir, 'test_730_003')
+        env.mkpath(testpath)
         # 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}")
index 9ad79f0b1e98db31eca6d8a1bde4442365482e0e..958f13f4d1331958fc87bdd5fc6a8cd96fe0d7a8 100644 (file)
@@ -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.*',
+            ]
+        )
index 5c0360251b5d022943e04852fb7b0f90bcdec310..7992337964327eff04272d95eb8c12de5804d2a3 100644 (file)
@@ -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
@@ -333,13 +335,16 @@ class TestStapling:
         md = self.mdA
         domains = [md]
         testpath = os.path.join(env.gen_dir, 'test_801_009')
+        env.mkpath(testpath)
         # 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)
index b18cfd38d447b217560731e05f819b11578a2b3b..c0018393e71d11c3e556fabfeb0a9109def00f4c 100644 (file)
@@ -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,16 @@ 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)
+        env.mkpath(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 +183,16 @@ 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)
+        env.mkpath(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)
index 6ad708728c771bda1b21f483b7f8294c5a8ff49b..306b131a16d08974753e99850b2157fe0cbf30ef 100644 (file)
@@ -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,16 @@ 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)
+        env.mkpath(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}
index 5519f16188bf46866da6b45ac680450a0b55071e..a08d5e64e4f322126dc20fc78664da5820c41d06 100644 (file)
@@ -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)