summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--changes-entries/md_v2.4.28.txt11
-rw-r--r--modules/md/md_acme_drive.c10
-rw-r--r--modules/md/md_reg.c6
-rw-r--r--modules/md/md_version.h4
-rwxr-xr-xtest/modules/md/conftest.py8
-rwxr-xr-xtest/modules/md/md_cert_util.py70
-rwxr-xr-xtest/modules/md/md_env.py27
-rw-r--r--test/modules/md/test_502_acmev2_drive.py14
-rw-r--r--test/modules/md/test_702_auto.py54
-rw-r--r--test/modules/md/test_730_static.py33
-rw-r--r--test/modules/md/test_741_setup_errors.py26
-rw-r--r--test/modules/md/test_801_stapling.py12
-rw-r--r--test/modules/md/test_901_message.py28
-rw-r--r--test/modules/md/test_920_status.py15
-rw-r--r--test/pyhttpd/certs.py83
15 files changed, 254 insertions, 147 deletions
diff --git a/changes-entries/md_v2.4.28.txt b/changes-entries/md_v2.4.28.txt
new file mode 100644
index 0000000000..3eb2bc4917
--- /dev/null
+++ b/changes-entries/md_v2.4.28.txt
@@ -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]
diff --git a/modules/md/md_acme_drive.c b/modules/md/md_acme_drive.c
index 4bb04f321c..0ec409c863 100644
--- a/modules/md/md_acme_drive.c
+++ b/modules/md/md_acme_drive.c
@@ -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))) {
diff --git a/modules/md/md_reg.c b/modules/md/md_reg.c
index 6aa7d78876..dc49446ae4 100644
--- a/modules/md/md_reg.c
+++ b/modules/md/md_reg.c
@@ -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);
}
}
diff --git a/modules/md/md_version.h b/modules/md/md_version.h
index cefbb8ded7..3e2914d6b6 100644
--- a/modules/md/md_version.h
+++ b/modules/md/md_version.h
@@ -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"
diff --git a/test/modules/md/conftest.py b/test/modules/md/conftest.py
index a7b064b6a9..0118de5e13 100755
--- a/test/modules/md/conftest.py
+++ b/test/modules/md/conftest.py
@@ -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
diff --git a/test/modules/md/md_cert_util.py b/test/modules/md/md_cert_util.py
index abcd36b938..6cd034a02b 100755
--- a/test/modules/md/md_cert_util.py
+++ b/test/modules/md/md_cert_util.py
@@ -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()
diff --git a/test/modules/md/md_env.py b/test/modules/md/md_env.py
index 360086f97b..acc8417b14 100755
--- a/test/modules/md/md_env.py
+++ b/test/modules/md/md_env.py
@@ -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)
diff --git a/test/modules/md/test_502_acmev2_drive.py b/test/modules/md/test_502_acmev2_drive.py
index eb754f25ef..b064647450 100644
--- a/test/modules/md/test_502_acmev2_drive.py
+++ b/test/modules/md/test_502_acmev2_drive.py
@@ -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)
diff --git a/test/modules/md/test_702_auto.py b/test/modules/md/test_702_auto.py
index 04a9c7561a..90103e3aff 100644
--- a/test/modules/md/test_702_auto.py
+++ b/test/modules/md/test_702_auto.py
@@ -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
diff --git a/test/modules/md/test_730_static.py b/test/modules/md/test_730_static.py
index 891ae620bb..209d33a1aa 100644
--- a/test/modules/md/test_730_static.py
+++ b/test/modules/md/test_730_static.py
@@ -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}")
diff --git a/test/modules/md/test_741_setup_errors.py b/test/modules/md/test_741_setup_errors.py
index 9ad79f0b1e..958f13f4d1 100644
--- a/test/modules/md/test_741_setup_errors.py
+++ b/test/modules/md/test_741_setup_errors.py
@@ -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.*',
+ ]
+ )
diff --git a/test/modules/md/test_801_stapling.py b/test/modules/md/test_801_stapling.py
index 5c0360251b..1eacfabf88 100644
--- a/test/modules/md/test_801_stapling.py
+++ b/test/modules/md/test_801_stapling.py
@@ -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)
diff --git a/test/modules/md/test_901_message.py b/test/modules/md/test_901_message.py
index b18cfd38d4..d5d66e6e1a 100644
--- a/test/modules/md/test_901_message.py
+++ b/test/modules/md/test_901_message.py
@@ -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)
diff --git a/test/modules/md/test_920_status.py b/test/modules/md/test_920_status.py
index 6ad708728c..6a5b338559 100644
--- a/test/modules/md/test_920_status.py
+++ b/test/modules/md/test_920_status.py
@@ -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}
diff --git a/test/pyhttpd/certs.py b/test/pyhttpd/certs.py
index 5519f16188..a08d5e64e4 100644
--- a/test/pyhttpd/certs.py
+++ b/test/pyhttpd/certs.py
@@ -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)