diff --git a/acme/src/acme/_internal/tests/crypto_util_test.py b/acme/src/acme/_internal/tests/crypto_util_test.py index a59ddbe21..9d781ec4c 100644 --- a/acme/src/acme/_internal/tests/crypto_util_test.py +++ b/acme/src/acme/_internal/tests/crypto_util_test.py @@ -112,21 +112,24 @@ class GenMakeSelfSignedCertTest(unittest.TestCase): self.serial_num: list[int] = [] self.privkey = rsa.generate_private_key(public_exponent=65537, key_size=2048) - def test_sn_collisions(self): + @classmethod + def _call(cls, *args, **kwargs): from acme.crypto_util import make_self_signed_cert + with pytest.warns(DeprecationWarning, match='make_self_signed_cert is deprecated'): + return make_self_signed_cert(*args, **kwargs) + + def test_sn_collisions(self): for _ in range(self.cert_count): - cert = make_self_signed_cert(self.privkey, ['dummy'], force_san=True, - ips=[ipaddress.ip_address("10.10.10.10")]) + cert = self._call(self.privkey, ['dummy'], force_san=True, + ips=[ipaddress.ip_address("10.10.10.10")]) self.serial_num.append(cert.serial_number) assert len(set(self.serial_num)) >= self.cert_count def test_no_ips(self): - from acme.crypto_util import make_self_signed_cert - make_self_signed_cert(self.privkey, ['dummy']) + self._call(self.privkey, ['dummy']) @mock.patch("acme.crypto_util._now") def test_expiry_times(self, mock_now): - from acme.crypto_util import make_self_signed_cert from datetime import datetime, timedelta, timezone not_before = 1736200830 validity = 100 @@ -134,7 +137,7 @@ class GenMakeSelfSignedCertTest(unittest.TestCase): not_before_dt = datetime.fromtimestamp(not_before) validity_td = timedelta(validity) not_after_dt = not_before_dt + validity_td - cert = make_self_signed_cert( + cert = self._call( self.privkey, ['dummy'], not_before=not_before_dt, @@ -155,7 +158,7 @@ class GenMakeSelfSignedCertTest(unittest.TestCase): now_dt = datetime.fromtimestamp(now) mock_now.return_value = now_dt.replace(tzinfo=timezone.utc) valid_after_now_dt = now_dt + validity_td - cert = make_self_signed_cert( + cert = self._call( self.privkey, ['dummy'], validity=validity_td, @@ -169,20 +172,18 @@ class GenMakeSelfSignedCertTest(unittest.TestCase): self.assertEqual(cert.not_valid_after, valid_after_now_dt) def test_no_name(self): - from acme.crypto_util import make_self_signed_cert with pytest.raises(AssertionError): - make_self_signed_cert(self.privkey, ips=[ipaddress.ip_address("1.1.1.1")]) - make_self_signed_cert(self.privkey) + self._call(self.privkey, ips=[ipaddress.ip_address("1.1.1.1")]) + self._call(self.privkey) def test_extensions(self): - from acme.crypto_util import make_self_signed_cert extension_type = x509.TLSFeature([x509.TLSFeatureType.status_request]) extension = x509.Extension( x509.TLSFeature.oid, False, extension_type ) - cert = make_self_signed_cert( + cert = self._call( self.privkey, ips=[ipaddress.ip_address("1.1.1.1")], extensions=[extension] diff --git a/acme/src/acme/crypto_util.py b/acme/src/acme/crypto_util.py index e01ae195e..e1f98d486 100644 --- a/acme/src/acme/crypto_util.py +++ b/acme/src/acme/crypto_util.py @@ -7,6 +7,7 @@ import typing from typing import Literal from typing import Optional from typing import Union +import warnings from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization @@ -202,6 +203,8 @@ def make_self_signed_cert(private_key: types.CertificateIssuerPrivateKeyTypes, subject CN. If only one domain is provided no ``subjectAltName`` extension is used, unless `force_san` is ``True``. """ + warnings.warn("make_self_signed_cert is deprecated and will be removed in " + "an upcoming release", DeprecationWarning) assert domains or ips, "Must provide one or more hostnames or IPs for the cert." builder = x509.CertificateBuilder() diff --git a/certbot-compatibility-test/src/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/src/certbot_compatibility_test/test_driver.py index d5e661d2b..d7b7de5c5 100644 --- a/certbot-compatibility-test/src/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/src/certbot_compatibility_test/test_driver.py @@ -18,7 +18,6 @@ from cryptography.hazmat.primitives import serialization from urllib3.util import connection from acme import challenges -from acme import crypto_util from acme import messages from certbot import achallenges from certbot import errors as le_errors @@ -143,7 +142,7 @@ def test_installer(args: argparse.Namespace, plugin: common.Proxy, config: str, def test_deploy_cert(plugin: common.Proxy, temp_dir: str, domains: list[str]) -> bool: """Tests deploy_cert returning True if the tests are successful""" - cert = crypto_util.make_self_signed_cert(util.KEY, domains) + cert = util.make_self_signed_cert(util.KEY, domains) cert_path = os.path.join(temp_dir, "cert.pem") with open(cert_path, "wb") as f: f.write(cert.public_bytes(serialization.Encoding.PEM)) diff --git a/certbot-compatibility-test/src/certbot_compatibility_test/util.py b/certbot-compatibility-test/src/certbot_compatibility_test/util.py index 9aba0e0e3..d6cbf8321 100644 --- a/certbot-compatibility-test/src/certbot_compatibility_test/util.py +++ b/certbot-compatibility-test/src/certbot_compatibility_test/util.py @@ -1,11 +1,15 @@ """Utility functions for Certbot plugin tests.""" import argparse import copy +from datetime import datetime, timedelta, timezone import os import re import shutil import tarfile +from cryptography import x509 +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import types import josepy as jose from certbot._internal import constants @@ -49,3 +53,50 @@ def extract_configs(configs: str, parent_dir: str) -> str: raise errors.Error("Unknown configurations file type") return config_dir + + +def make_self_signed_cert(private_key: types.CertificateIssuerPrivateKeyTypes, + domains: list[str], + ) -> x509.Certificate: + """Generate new self-signed certificate. + + :param buffer private_key_pem: Private key, in PEM PKCS#8 format. + :type domains: `list` of `str` + :param buffer private_key_pem: One of + `cryptography.hazmat.primitives.asymmetric.types.CertificateIssuerPrivateKeyTypes` + + If more than one domain is provided, all of the domains are put into + ``subjectAltName`` X.509 extension and first domain is set as the + subject CN. If only one domain is provided no ``subjectAltName`` + extension is used. + + """ + assert domains, "Must provide one or more hostnames for the cert." + + builder = x509.CertificateBuilder() + builder = builder.serial_number(x509.random_serial_number()) + + builder = builder.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True) + + name_attrs = [x509.NameAttribute(x509.OID_COMMON_NAME, domains[0])] + + builder = builder.subject_name(x509.Name(name_attrs)) + builder = builder.issuer_name(x509.Name(name_attrs)) + + sanlist: list[x509.GeneralName] = [] + for address in domains: + sanlist.append(x509.DNSName(address)) + if len(domains) > 1: + builder = builder.add_extension( + x509.SubjectAlternativeName(sanlist), + critical=False + ) + + not_before = datetime.now(tz=timezone.utc) + validity = timedelta(seconds=7 * 24 * 60 * 60) + builder = builder.not_valid_before(not_before) + builder = builder.not_valid_after(not_before + validity) + + public_key = private_key.public_key() + builder = builder.public_key(public_key) + return builder.sign(private_key, hashes.SHA256()) diff --git a/newsfragments/10466.changed b/newsfragments/10466.changed new file mode 100644 index 000000000..69f579707 --- /dev/null +++ b/newsfragments/10466.changed @@ -0,0 +1 @@ +The function `acme.crypto_util.make_self_signed_cert` was deprecated and will be removed in a future release.