summaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
authorStefan Eissing <icing@apache.org>2024-09-17 13:38:19 +0200
committerStefan Eissing <icing@apache.org>2024-09-17 13:38:19 +0200
commitaf10058840e024e6cf2bbdd50cc1dacfe236e6fc (patch)
tree1bba86e6826aca4314c202ee7b643059d40c36f6 /test
parentremoved experimental mod_tls. source, documenation and test cases (diff)
downloadapache2-af10058840e024e6cf2bbdd50cc1dacfe236e6fc.tar.xz
apache2-af10058840e024e6cf2bbdd50cc1dacfe236e6fc.zip
*) 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/trunk@1920747 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'test')
-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
11 files changed, 232 insertions, 138 deletions
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)