Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-acme for openSUSE:Factory checked in at 2025-04-22 17:28:56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-acme (Old) and /work/SRC/openSUSE:Factory/.python-acme.new.30101 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-acme" Tue Apr 22 17:28:56 2025 rev:73 rq:1271239 version:4.0.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-acme/python-acme.changes 2025-03-21 20:23:38.205791507 +0100 +++ /work/SRC/openSUSE:Factory/.python-acme.new.30101/python-acme.changes 2025-04-22 17:29:38.318497910 +0200 @@ -1,0 +2,22 @@ +Tue Apr 22 03:28:47 UTC 2025 - Steve Kowalik <steven.kowa...@suse.com> + +- Update to 4.0.0: + * Added + + The --preferred-profile and --required-profile flags allow requesting + a profile. + * Changed + + Certificates now renew with 1/3rd of lifetime left (or 1/2 of lifetime + left, if the lifetime is shorter than 10 days). + + removed acme.crypto_util._pyopenssl_cert_or_req_all_names + + removed acme.crypto_util._pyopenssl_cert_or_req_san + + removed acme.crypto_util.dump_pyopenssl_chain + + removed acme.crypto_util.gen_ss_cert + + removed certbot.crypto_util.dump_pyopenssl_chain + + removed certbot.crypto_util.pyopenssl_load_certificate + * Fixed + + Moved RewriteEngine on directive added during apache http01 + authentication to the end of the virtual host, so that it overwrites + any RewriteEngine off directives that already exist and allows + redirection to the challenge URL. + +------------------------------------------------------------------- Old: ---- acme-3.3.0.tar.gz New: ---- acme-4.0.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-acme.spec ++++++ --- /var/tmp/diff_new_pack.3P6fKE/_old 2025-04-22 17:29:38.866520930 +0200 +++ /var/tmp/diff_new_pack.3P6fKE/_new 2025-04-22 17:29:38.866520930 +0200 @@ -19,14 +19,14 @@ %{?sle15_python_module_pythons} %define libname acme Name: python-%{libname} -Version: 3.3.0 +Version: 4.0.0 Release: 0 Summary: Python library for the ACME protocol License: Apache-2.0 URL: https://github.com/certbot/certbot Source0: https://files.pythonhosted.org/packages/source/a/%{libname}/%{libname}-%{version}.tar.gz BuildRequires: %{python_module cryptography >= 43.0.0} -BuildRequires: %{python_module josepy >= 1.13.0} +BuildRequires: %{python_module josepy >= 2.0.0} BuildRequires: %{python_module pip} BuildRequires: %{python_module pyOpenSSL >= 25.0.0} BuildRequires: %{python_module pyRFC3339} @@ -38,7 +38,7 @@ BuildRequires: fdupes BuildRequires: python-rpm-macros Requires: python-cryptography >= 43.0.0 -Requires: python-josepy >= 1.13.0 +Requires: python-josepy >= 2.0.0 Requires: python-pyOpenSSL >= 25.0.0 Requires: python-pyRFC3339 Requires: python-pytz >= 2019.3 @@ -61,7 +61,6 @@ %install %pyproject_install -# remove duplicates %python_expand %fdupes %{buildroot}%{$python_sitelib}/%{libname} %check @@ -71,5 +70,5 @@ %license LICENSE.txt %pycache_only %{python_sitelib}/%{libname}/__pycache__ %{python_sitelib}/%{libname} -%{python_sitelib}/%{libname}-%{version}*info +%{python_sitelib}/%{libname}-%{version}.dist-info ++++++ acme-3.3.0.tar.gz -> acme-4.0.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-3.3.0/PKG-INFO new/acme-4.0.0/PKG-INFO --- old/acme-3.3.0/PKG-INFO 2025-03-11 16:03:30.108355800 +0100 +++ new/acme-4.0.0/PKG-INFO 2025-04-08 00:03:34.974550000 +0200 @@ -1,6 +1,6 @@ -Metadata-Version: 2.2 +Metadata-Version: 2.4 Name: acme -Version: 3.3.0 +Version: 4.0.0 Summary: ACME protocol implementation in Python Home-page: https://github.com/certbot/certbot Author: Certbot Project @@ -21,7 +21,7 @@ Requires-Python: >=3.9 License-File: LICENSE.txt Requires-Dist: cryptography>=43.0.0 -Requires-Dist: josepy<2,>=1.13.0 +Requires-Dist: josepy>=2.0.0 Requires-Dist: PyOpenSSL>=25.0.0 Requires-Dist: pyrfc3339 Requires-Dist: pytz>=2019.3 @@ -38,6 +38,7 @@ Dynamic: classifier Dynamic: home-page Dynamic: license +Dynamic: license-file Dynamic: provides-extra Dynamic: requires-dist Dynamic: requires-python diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-3.3.0/acme/_internal/tests/challenges_test.py new/acme-4.0.0/acme/_internal/tests/challenges_test.py --- old/acme-3.3.0/acme/_internal/tests/challenges_test.py 2025-03-11 16:03:28.000000000 +0100 +++ new/acme-4.0.0/acme/_internal/tests/challenges_test.py 2025-04-08 00:03:33.000000000 +0200 @@ -13,7 +13,7 @@ from acme import errors from acme._internal.tests import test_util -CERT = test_util.load_comparable_cert('cert.pem') +CERT = test_util.load_cert('cert.pem') KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem')) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-3.3.0/acme/_internal/tests/crypto_util_test.py new/acme-4.0.0/acme/_internal/tests/crypto_util_test.py --- old/acme-3.3.0/acme/_internal/tests/crypto_util_test.py 2025-03-11 16:03:28.000000000 +0100 +++ new/acme-4.0.0/acme/_internal/tests/crypto_util_test.py 2025-04-08 00:03:33.000000000 +0200 @@ -11,8 +11,6 @@ from unittest import mock import warnings -import josepy as jose -import OpenSSL import pytest from cryptography import x509 from cryptography.hazmat.primitives import serialization @@ -33,10 +31,10 @@ """Tests for acme.crypto_util.SSLSocket/probe_sni.""" def setUp(self): - self.cert = test_util.load_comparable_cert('rsa2048_cert.pem') + self.cert = test_util.load_cert('rsa2048_cert.pem') key = test_util.load_pyopenssl_private_key('rsa2048_key.pem') # pylint: disable=protected-access - certs = {b'foo': (key, self.cert.wrapped)} + certs = {b'foo': (key, self.cert)} from acme.crypto_util import SSLSocket @@ -58,8 +56,7 @@ def _probe(self, name): from acme.crypto_util import probe_sni - return jose.ComparableX509(probe_sni( - name, host='127.0.0.1', port=self.port)) + return probe_sni(name, host='127.0.0.1', port=self.port) def _start_server(self): self.server_thread.start() @@ -97,48 +94,29 @@ _ = SSLSocket(None) -class PyOpenSSLCertOrReqAllNamesTest(unittest.TestCase): - """Test for acme.crypto_util._pyopenssl_cert_or_req_all_names.""" +class MiscTests(unittest.TestCase): - @classmethod - def _call(cls, loader, name): - # pylint: disable=protected-access - from acme.crypto_util import _pyopenssl_cert_or_req_all_names - with warnings.catch_warnings(): - warnings.filterwarnings( - 'ignore', - message='acme.crypto_util._pyopenssl_cert_or_req_all_names is deprecated *' - ) - return _pyopenssl_cert_or_req_all_names(loader(name)) - - def _call_cert(self, name): - return self._call(test_util.load_cert, name) + def test_dump_cryptography_chain(self): + from acme.crypto_util import dump_cryptography_chain - def test_cert_one_san_no_common(self): - assert self._call_cert('cert-nocn.der') == \ - ['no-common-name.badssl.com'] + cert1 = test_util.load_cert('rsa2048_cert.pem') + cert2 = test_util.load_cert('rsa4096_cert.pem') - def test_cert_no_sans_yes_common(self): - assert self._call_cert('cert.pem') == ['example.com'] + chain = [cert1, cert2] + dumped = dump_cryptography_chain(chain) - def test_cert_two_sans_yes_common(self): - assert self._call_cert('cert-san.pem') == \ - ['example.com', 'www.example.com'] + # default is PEM encoding Encoding.PEM + assert isinstance(dumped, bytes) -class PyOpenSSLCertOrReqSANTest(unittest.TestCase): - """Test for acme.crypto_util._pyopenssl_cert_or_req_san.""" +class CryptographyCertOrReqSANTest(unittest.TestCase): + """Test for acme.crypto_util._cryptography_cert_or_req_san.""" @classmethod def _call(cls, loader, name): # pylint: disable=protected-access - from acme.crypto_util import _pyopenssl_cert_or_req_san - with warnings.catch_warnings(): - warnings.filterwarnings( - 'ignore', - message='acme.crypto_util._pyopenssl_cert_or_req_san is deprecated *' - ) - return _pyopenssl_cert_or_req_san(loader(name)) + from acme.crypto_util import _cryptography_cert_or_req_san + return _cryptography_cert_or_req_san(loader(name)) @classmethod def _get_idn_names(cls): @@ -285,41 +263,6 @@ self.assertIn(extension, cert.extensions) -class GenSsCertTest(unittest.TestCase): - """Test for gen_ss_cert (generation of self-signed cert).""" - - - def setUp(self): - self.cert_count = 5 - self.serial_num: List[int] = [] - self.key = OpenSSL.crypto.PKey() - self.key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) - - def test_sn_collisions(self): - from acme.crypto_util import gen_ss_cert - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - for _ in range(self.cert_count): - cert = gen_ss_cert(self.key, ['dummy'], force_san=True, - ips=[ipaddress.ip_address("10.10.10.10")]) - self.serial_num.append(cert.get_serial_number()) - assert len(set(self.serial_num)) >= self.cert_count - - def test_no_name(self): - from acme.crypto_util import gen_ss_cert - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - with pytest.raises(AssertionError): - gen_ss_cert(self.key, ips=[ipaddress.ip_address("1.1.1.1")]) - gen_ss_cert(self.key) - - def test_no_ips(self): - from acme.crypto_util import gen_ss_cert - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - gen_ss_cert(self.key, ['dummy']) - - class MakeCSRTest(unittest.TestCase): """Test for standalone functions.""" @@ -395,42 +338,5 @@ make_csr(privkey_pem, ["a.example"]) -class DumpPyopensslChainTest(unittest.TestCase): - """Test for dump_pyopenssl_chain.""" - - @classmethod - def _call(cls, loaded): - # pylint: disable=protected-access - from acme.crypto_util import dump_pyopenssl_chain - with warnings.catch_warnings(): - warnings.filterwarnings( - 'ignore', - message='acme.crypto_util.dump_pyopenssl_chain is *' - ) - return dump_pyopenssl_chain(loaded) - - def test_dump_pyopenssl_chain(self): - names = ['cert.pem', 'cert-san.pem', 'cert-idnsans.pem'] - loaded = [test_util.load_cert(name) for name in names] - length = sum( - len(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)) - for cert in loaded) - assert len(self._call(loaded)) == length - - def test_dump_pyopenssl_chain_wrapped(self): - names = ['cert.pem', 'cert-san.pem', 'cert-idnsans.pem'] - loaded = [test_util.load_cert(name) for name in names] - wrap_func = jose.ComparableX509 - with warnings.catch_warnings(): - warnings.filterwarnings( - 'ignore', - message='The next major version of josepy *' - ) - wrapped = [wrap_func(cert) for cert in loaded] - dump_func = OpenSSL.crypto.dump_certificate - length = sum(len(dump_func(OpenSSL.crypto.FILETYPE_PEM, cert)) for cert in loaded) - assert len(self._call(wrapped)) == length - - if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-3.3.0/acme/_internal/tests/jose_test.py new/acme-4.0.0/acme/_internal/tests/jose_test.py --- old/acme-3.3.0/acme/_internal/tests/jose_test.py 2025-03-11 16:03:28.000000000 +0100 +++ new/acme-4.0.0/acme/_internal/tests/jose_test.py 2025-04-08 00:03:33.000000000 +0200 @@ -1,7 +1,6 @@ """Tests for acme.jose shim.""" import importlib import sys -import unittest import pytest diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-3.3.0/acme/_internal/tests/messages_test.py new/acme-4.0.0/acme/_internal/tests/messages_test.py --- old/acme-3.3.0/acme/_internal/tests/messages_test.py 2025-03-11 16:03:28.000000000 +0100 +++ new/acme-4.0.0/acme/_internal/tests/messages_test.py 2025-04-08 00:03:33.000000000 +0200 @@ -1,10 +1,8 @@ """Tests for acme.messages.""" -import contextlib import sys from typing import Dict import unittest from unittest import mock -import warnings import josepy as jose import pytest @@ -12,8 +10,8 @@ from acme import challenges from acme._internal.tests import test_util -CERT = test_util.load_comparable_cert('cert.der') -CSR = test_util.load_comparable_csr('csr.der') +CERT = test_util.load_cert('cert.der') +CSR = test_util.load_csr('csr.der') KEY = test_util.load_rsa_private_key('rsa512_key.pem') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-3.3.0/acme/_internal/tests/standalone_test.py new/acme-4.0.0/acme/_internal/tests/standalone_test.py --- old/acme-3.3.0/acme/_internal/tests/standalone_test.py 2025-03-11 16:03:28.000000000 +0100 +++ new/acme-4.0.0/acme/_internal/tests/standalone_test.py 2025-04-08 00:03:33.000000000 +0200 @@ -144,8 +144,8 @@ # cert = crypto_util.probe_sni( # b'localhost', host=host, port=port, timeout=1) # # Expect normal cert when connecting without ALPN. - # self.assertEqual(jose.ComparableX509(cert), - # jose.ComparableX509(self.certs[b'localhost'][1])) + # self.assertEqual(cert, + # self.certs[b'localhost'][1]) def test_challenge_certs(self): host, port = self.server.socket.getsockname()[:2] @@ -153,7 +153,7 @@ b'localhost', host=host, port=port, timeout=1, alpn_protocols=[b"acme-tls/1"]) # Expect challenge cert when connecting with ALPN. - assert cert.to_cryptography() == self.challenge_certs[b'localhost'][1] + assert cert == self.challenge_certs[b'localhost'][1] def test_bad_alpn(self): host, port = self.server.socket.getsockname()[:2] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-3.3.0/acme/_internal/tests/test_util.py new/acme-4.0.0/acme/_internal/tests/test_util.py --- old/acme-3.3.0/acme/_internal/tests/test_util.py 2025-03-11 16:03:28.000000000 +0100 +++ new/acme-4.0.0/acme/_internal/tests/test_util.py 2025-04-08 00:03:33.000000000 +0200 @@ -5,8 +5,9 @@ """ import importlib.resources import os -import sys +from typing import Callable +from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization import josepy as jose @@ -21,37 +22,36 @@ return vector_ref.read_bytes() -def _guess_loader(filename, loader_pem, loader_der): +def _guess_loader(filename: str, loader_pem: Callable, loader_der: Callable) -> Callable: _, ext = os.path.splitext(filename) - if ext.lower() == '.pem': + if ext.lower() == ".pem": return loader_pem - elif ext.lower() == '.der': + elif ext.lower() == ".der": return loader_der - raise ValueError("Loader could not be recognized based on extension") # pragma: no cover + else: # pragma: no cover + raise ValueError("Loader could not be recognized based on extension") -def load_cert(*names): - """Load certificate.""" - loader = _guess_loader( - names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) - return crypto.load_certificate(loader, load_vector(*names)) - - -def load_comparable_cert(*names): - """Load ComparableX509 cert.""" - return jose.ComparableX509(load_cert(*names)) +def _guess_pyopenssl_loader(filename: str, loader_pem: int, loader_der: int) -> int: + _, ext = os.path.splitext(filename) + if ext.lower() == ".pem": + return loader_pem + else: # pragma: no cover + raise ValueError("Loader could not be recognized based on extension") -def load_csr(*names): - """Load certificate request.""" +def load_cert(*names: str) -> x509.Certificate: + """Load certificate.""" loader = _guess_loader( - names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) - return crypto.load_certificate_request(loader, load_vector(*names)) + names[-1], x509.load_pem_x509_certificate, x509.load_der_x509_certificate + ) + return loader(load_vector(*names)) -def load_comparable_csr(*names): - """Load ComparableX509 certificate request.""" - return jose.ComparableX509(load_csr(*names)) +def load_csr(*names: str) -> x509.CertificateSigningRequest: + """Load certificate request.""" + loader = _guess_loader(names[-1], x509.load_pem_x509_csr, x509.load_der_x509_csr) + return loader(load_vector(*names)) def load_rsa_private_key(*names): @@ -72,6 +72,6 @@ def load_pyopenssl_private_key(*names): """Load pyOpenSSL private key.""" - loader = _guess_loader( + loader = _guess_pyopenssl_loader( names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) return crypto.load_privatekey(loader, load_vector(*names)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-3.3.0/acme/_internal/tests/util_test.py new/acme-4.0.0/acme/_internal/tests/util_test.py --- old/acme-3.3.0/acme/_internal/tests/util_test.py 2025-03-11 16:03:28.000000000 +0100 +++ new/acme-4.0.0/acme/_internal/tests/util_test.py 2025-04-08 00:03:33.000000000 +0200 @@ -1,6 +1,5 @@ """Tests for acme.util.""" import sys -import unittest import pytest diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-3.3.0/acme/challenges.py new/acme-4.0.0/acme/challenges.py --- old/acme-3.3.0/acme/challenges.py 2025-03-11 16:03:28.000000000 +0100 +++ new/acme-4.0.0/acme/challenges.py 2025-04-08 00:03:33.000000000 +0200 @@ -419,7 +419,7 @@ return hashlib.sha256(self.key_authorization.encode('utf-8')).digest() def gen_cert(self, domain: str, key: Optional[crypto.PKey] = None, bits: int = 2048 - ) -> Tuple[crypto.X509, crypto.PKey]: + ) -> Tuple[x509.Certificate, crypto.PKey]: """Generate tls-alpn-01 certificate. :param str domain: Domain verified by the challenge. @@ -428,7 +428,7 @@ fresh key will be generated. :param int bits: Number of bits for newly generated key. - :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` + :rtype: `tuple` of `x509.Certificate` and `OpenSSL.crypto.PKey` """ if key is None: @@ -450,10 +450,10 @@ force_san=True, extensions=[acme_extension] ) - return crypto.X509.from_cryptography(cert), key + return cert, key def probe_cert(self, domain: str, host: Optional[str] = None, - port: Optional[int] = None) -> crypto.X509: + port: Optional[int] = None) -> x509.Certificate: """Probe tls-alpn-01 challenge certificate. :param str domain: domain being validated, required. @@ -470,20 +470,17 @@ return crypto_util.probe_sni(host=host.encode(), port=port, name=domain.encode(), alpn_protocols=[self.ACME_TLS_1_PROTOCOL]) - def verify_cert(self, domain: str, cert: Union[x509.Certificate, crypto.X509]) -> bool: + def verify_cert(self, domain: str, cert: x509.Certificate, ) -> bool: """Verify tls-alpn-01 challenge certificate. :param str domain: Domain name being validated. :param cert: Challenge certificate. - :type cert: `cryptography.x509.Certificate` or `OpenSSL.crypto.X509` + :type cert: `cryptography.x509.Certificate` :returns: Whether the certificate was successfully verified. :rtype: bool """ - if not isinstance(cert, x509.Certificate): - cert = cert.to_cryptography() - names = crypto_util.get_names_from_subject_and_extensions( cert.subject, cert.extensions ) @@ -506,7 +503,7 @@ # pylint: disable=too-many-arguments def simple_verify(self, chall: 'TLSALPN01', domain: str, account_public_key: jose.JWK, - cert: Optional[crypto.X509] = None, host: Optional[str] = None, + cert: Optional[x509.Certificate] = None, host: Optional[str] = None, port: Optional[int] = None) -> bool: """Simple verify. @@ -516,7 +513,7 @@ :param .challenges.TLSALPN01 chall: Corresponding challenge. :param str domain: Domain name being validated. :param JWK account_public_key: - :param OpenSSL.crypto.X509 cert: Optional certificate. If not + :param x509.Certificate cert: Optional certificate. If not provided (``None``) certificate will be retrieved using `probe_cert`. :param string host: IP address used to probe the certificate. @@ -547,7 +544,8 @@ response_cls = TLSALPN01Response typ = response_cls.typ - def validation(self, account_key: jose.JWK, **kwargs: Any) -> Tuple[crypto.X509, crypto.PKey]: + def validation(self, account_key: jose.JWK, + **kwargs: Any) -> Tuple[x509.Certificate, crypto.PKey]: """Generate validation. :param JWK account_key: @@ -556,7 +554,7 @@ in certificate generation. If not provided (``None``), then fresh key will be generated. - :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` + :rtype: `tuple` of `x509.Certificate` and `OpenSSL.crypto.PKey` """ # TODO: Remove cast when response() is generic. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-3.3.0/acme/client.py new/acme-4.0.0/acme/client.py --- old/acme-3.3.0/acme/client.py 2025-03-11 16:03:28.000000000 +0100 +++ new/acme-4.0.0/acme/client.py 2025-04-08 00:03:33.000000000 +0200 @@ -14,12 +14,10 @@ from typing import Set from typing import Tuple from typing import Union -import warnings from cryptography import x509 import josepy as jose -import OpenSSL import requests from requests.adapters import HTTPAdapter from requests.utils import parse_header_links @@ -227,12 +225,8 @@ :returns: updated order :rtype: messages.OrderResource """ - csr = OpenSSL.crypto.load_certificate_request( - OpenSSL.crypto.FILETYPE_PEM, orderr.csr_pem) - with warnings.catch_warnings(): - warnings.filterwarnings('ignore', - message='The next major version of josepy will remove josepy.util.ComparableX509') - wrapped_csr = messages.CertificateRequest(csr=jose.ComparableX509(csr)) + csr = x509.load_pem_x509_csr(orderr.csr_pem) + wrapped_csr = messages.CertificateRequest(csr=csr) res = self._post(orderr.body.finalize, wrapped_csr) orderr = orderr.update(body=messages.Order.from_json(res.json())) return orderr @@ -285,11 +279,10 @@ self.begin_finalization(orderr) return self.poll_finalization(orderr, deadline, fetch_alternative_chains) - def revoke(self, cert: jose.ComparableX509, rsn: int) -> None: + def revoke(self, cert: x509.Certificate, rsn: int) -> None: """Revoke certificate. - :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in - `.ComparableX509` + :param x509.Certificate cert: `x509.Certificate` :param int rsn: Reason code for certificate revocation. @@ -477,11 +470,10 @@ return datetime.datetime.now() + datetime.timedelta(seconds=seconds) - def _revoke(self, cert: jose.ComparableX509, rsn: int, url: str) -> None: + def _revoke(self, cert: x509.Certificate, rsn: int, url: str) -> None: """Revoke certificate. - :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in - `.ComparableX509` + :param .x509.Certificate cert: `x509.Certificate` :param int rsn: Reason code for certificate revocation. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-3.3.0/acme/crypto_util.py new/acme-4.0.0/acme/crypto_util.py --- old/acme-3.3.0/acme/crypto_util.py 2025-03-11 16:03:28.000000000 +0100 +++ new/acme-4.0.0/acme/crypto_util.py 2025-04-08 00:03:33.000000000 +0200 @@ -1,16 +1,15 @@ """Crypto utilities.""" -import binascii import contextlib import enum from datetime import datetime, timedelta, timezone import ipaddress import logging -import os import socket import typing from typing import Any from typing import Callable from typing import List +from typing import Literal from typing import Mapping from typing import Optional from typing import Sequence @@ -21,23 +20,22 @@ from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import dsa, rsa, ec, ed25519, ed448, types -import josepy as jose +from cryptography.hazmat.primitives.serialization import Encoding from OpenSSL import crypto from OpenSSL import SSL from acme import errors -import warnings logger = logging.getLogger(__name__) # Default SSL method selected here is the most compatible, while secure # SSL method: TLSv1_METHOD is only compatible with -# TLSv1_METHOD, while SSLv23_METHOD is compatible with all other +# TLSv1_METHOD, while TLS_method is compatible with all other # methods, including TLSv2_METHOD (read more at -# https://www.openssl.org/docs/ssl/SSLv23_method.html). _serve_sni +# https://docs.openssl.org/master/man3/SSL_CTX_new/#notes). _serve_sni # should be changed to use "set_options" to disable SSLv2 and SSLv3, # in case it's used for things other than probing/serving! -_DEFAULT_SSL_METHOD = SSL.SSLv23_METHOD +_DEFAULT_SSL_METHOD = SSL.TLS_METHOD class Format(enum.IntEnum): @@ -49,13 +47,13 @@ DER = crypto.FILETYPE_ASN1 PEM = crypto.FILETYPE_PEM - def to_cryptography_encoding(self) -> serialization.Encoding: + def to_cryptography_encoding(self) -> Encoding: """Converts the Format to the corresponding cryptography `Encoding`. """ if self == Format.DER: - return serialization.Encoding.DER + return Encoding.DER else: - return serialization.Encoding.PEM + return Encoding.PEM _KeyAndCert = Union[ @@ -74,6 +72,7 @@ return self.certs.get(server_name, None) return None # pragma: no cover + class SSLSocket: # pylint: disable=too-few-public-methods """SSL wrapper for sockets. @@ -133,9 +132,10 @@ return key, cert = pair new_context = SSL.Context(self.method) - new_context.set_options(SSL.OP_NO_SSLv2) - new_context.set_options(SSL.OP_NO_SSLv3) + new_context.set_min_proto_version(SSL.TLS1_2_VERSION) new_context.use_privatekey(key) + if isinstance(cert, x509.Certificate): + cert = crypto.X509.from_cryptography(cert) new_context.use_certificate(cert) if self.alpn_selection is not None: new_context.set_alpn_select_callback(self.alpn_selection) @@ -197,7 +197,7 @@ def probe_sni(name: bytes, host: bytes, port: int = 443, timeout: int = 300, # pylint: disable=too-many-arguments method: int = _DEFAULT_SSL_METHOD, source_address: Tuple[str, int] = ('', 0), - alpn_protocols: Optional[Sequence[bytes]] = None) -> crypto.X509: + alpn_protocols: Optional[Sequence[bytes]] = None) -> x509.Certificate: """Probe SNI server for SSL certificate. :param bytes name: Byte string to send as the server name in the @@ -215,7 +215,7 @@ :raises acme.errors.Error: In case of any problems. :returns: SSL certificate presented by the server. - :rtype: OpenSSL.crypto.X509 + :rtype: cryptography.x509.Certificate """ context = SSL.Context(method) @@ -249,21 +249,9 @@ raise errors.Error(error) cert = client_ssl.get_peer_certificate() assert cert # Appease mypy. We would have crashed out by now if there was no certificate. - return cert + return cert.to_cryptography() -# Annoyingly, we can't directly use cryptography's equivalent Union[] type for -# our type signatures since they're only public API in 40.0.x+, which is too new -# for some Certbot # distribution channels. Once we bump our oldest cryptography -# version past 40.0.x, usage of this type can be replaced with: -# cryptography.hazmat.primitives.asymmetric.types.CertificateIssuerPrivateKeyTypes -CertificateIssuerPrivateKeyTypes = Union[ - dsa.DSAPrivateKey, - rsa.RSAPrivateKey, - ec.EllipticCurvePrivateKey, - ed25519.Ed25519PrivateKey, - ed448.Ed448PrivateKey, -] # Even *more* annoyingly, due to a mypy bug, we can't use Union[] types in # isinstance expressions without causing false mypy errors. So we have to # recreate the type collection as a tuple here. And no, typing.get_args doesn't @@ -334,7 +322,7 @@ ) csr = builder.sign(private_key, hashes.SHA256()) - return csr.public_bytes(serialization.Encoding.PEM) + return csr.public_bytes(Encoding.PEM) def get_names_from_subject_and_extensions( @@ -371,32 +359,16 @@ return [cns[0]] + [d for d in dns_names if d != cns[0]] -def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req: Union[crypto.X509, crypto.X509Req] - ) -> List[str]: - """ - Deprecated - .. deprecated: 3.2.1 - """ - warnings.warn( - "acme.crypto_util._pyopenssl_cert_or_req_all_names is deprecated and " - "will be removed in the next major release of Certbot.", - DeprecationWarning, - stacklevel=2 - ) - cert_or_req = loaded_cert_or_req.to_cryptography() - return get_names_from_subject_and_extensions( - cert_or_req.subject, cert_or_req.extensions - ) - - -def _pyopenssl_cert_or_req_san(cert_or_req: Union[crypto.X509, crypto.X509Req]) -> List[str]: +def _cryptography_cert_or_req_san( + cert_or_req: Union[x509.Certificate, x509.CertificateSigningRequest], +) -> List[str]: """Get Subject Alternative Names from certificate or CSR using pyOpenSSL. .. note:: Although this is `acme` internal API, it is used by `letsencrypt`. :param cert_or_req: Certificate or CSR. - :type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`. + :type cert_or_req: `x509.Certificate` or `x509.CertificateSigningRequest`. :returns: A list of Subject Alternative Names that is DNS. :rtype: `list` of `str` @@ -404,13 +376,8 @@ Deprecated .. deprecated: 3.2.1 """ - warnings.warn( - "acme.crypto_util._pyopenssl_cert_or_req_san is deprecated and " - "will be removed in the next major release of Certbot.", - DeprecationWarning, - stacklevel=2 - ) - exts = cert_or_req.to_cryptography().extensions + # ???: is this translation needed? + exts = cert_or_req.extensions try: san_ext = exts.get_extension_for_class(x509.SubjectAlternativeName) except x509.ExtensionNotFound: @@ -424,7 +391,7 @@ return datetime.now(tz=timezone.utc) -def make_self_signed_cert(private_key: CertificateIssuerPrivateKeyTypes, +def make_self_signed_cert(private_key: types.CertificateIssuerPrivateKeyTypes, domains: Optional[List[str]] = None, not_before: Optional[datetime] = None, validity: Optional[timedelta] = None, force_san: bool = True, @@ -441,7 +408,8 @@ :param validity: Duration for which the cert will be valid. Defaults to 1 week :type validity: `datetime.timedelta` - :param buffer private_key_pem: One of `CertificateIssuerPrivateKeyTypes` + :param buffer private_key_pem: One of + `cryptography.hazmat.primitives.asymmetric.types.CertificateIssuerPrivateKeyTypes` :param bool force_san: :param extensions: List of additional extensions to include in the cert. :type extensions: `list` of `x509.Extension[x509.ExtensionType]` @@ -498,85 +466,13 @@ return builder.sign(private_key, hashes.SHA256()) -def gen_ss_cert(key: crypto.PKey, domains: Optional[List[str]] = None, - not_before: Optional[int] = None, - validity: int = (7 * 24 * 60 * 60), force_san: bool = True, - extensions: Optional[List[crypto.X509Extension]] = None, - ips: Optional[List[Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]] = None - ) -> crypto.X509: - """Generate new self-signed certificate. - - :type domains: `list` of `str` - :param OpenSSL.crypto.PKey key: - :param bool force_san: - :param extensions: List of additional extensions to include in the cert. - :type extensions: `list` of `OpenSSL.crypto.X509Extension` - :type ips: `list` of (`ipaddress.IPv4Address` or `ipaddress.IPv6Address`) - - 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, unless `force_san` is ``True``. - - .. deprecated: 2.10.0 - """ - warnings.warn( - "acme.crypto_util.gen_ss_cert is deprecated and will be removed in the " - "next major release of Certbot. Please use " - "acme.crypto_util.make_self_signed_cert instead.", DeprecationWarning, - stacklevel=2 - ) - assert domains or ips, "Must provide one or more hostnames or IPs for the cert." - - cert = crypto.X509() - cert.set_serial_number(int(binascii.hexlify(os.urandom(16)), 16)) - cert.set_version(2) - - if extensions is None: - extensions = [] - if domains is None: - domains = [] - if ips is None: - ips = [] - extensions.append( - crypto.X509Extension( - b"basicConstraints", True, b"CA:TRUE, pathlen:0"), - ) - - if len(domains) > 0: - cert.get_subject().CN = domains[0] - # TODO: what to put into cert.get_subject()? - cert.set_issuer(cert.get_subject()) - - sanlist = [] - for address in domains: - sanlist.append('DNS:' + address) - for ip in ips: - sanlist.append('IP:' + ip.exploded) - san_string = ', '.join(sanlist).encode('ascii') - if force_san or len(domains) > 1 or len(ips) > 0: - extensions.append(crypto.X509Extension( - b"subjectAltName", - critical=False, - value=san_string - )) - - cert.add_extensions(extensions) - - cert.gmtime_adj_notBefore(0 if not_before is None else not_before) - cert.gmtime_adj_notAfter(validity) - - cert.set_pubkey(key) - cert.sign(key, "sha256") - return cert - - -def dump_pyopenssl_chain(chain: Union[List[jose.ComparableX509], List[crypto.X509]], - filetype: Union[Format, int] = Format.PEM) -> bytes: +def dump_cryptography_chain( + chain: List[x509.Certificate], + encoding: Literal[Encoding.PEM, Encoding.DER] = Encoding.PEM, +) -> bytes: """Dump certificate chain into a bundle. - :param list chain: List of `OpenSSL.crypto.X509` (or wrapped in - :class:`josepy.util.ComparableX509`). + :param list chain: List of `cryptography.x509.Certificate`. :returns: certificate chain bundle :rtype: bytes @@ -584,24 +480,12 @@ Deprecated .. deprecated: 3.2.1 """ - warnings.warn( - "acme.crypto_util.dump_pyopenssl_chain is deprecated and " - "will be removed in the next major release of Certbot.", - DeprecationWarning, - stacklevel=2 - ) # XXX: returns empty string when no chain is available, which # shuts up RenewableCert, but might not be the best solution... - filetype = Format(filetype) - def _dump_cert(cert: Union[jose.ComparableX509, crypto.X509]) -> bytes: - if isinstance(cert, jose.ComparableX509): - if isinstance(cert.wrapped, crypto.X509Req): - raise errors.Error("Unexpected CSR provided.") # pragma: no cover - cert = cert.wrapped - - return cert.to_cryptography().public_bytes(filetype.to_cryptography_encoding()) + def _dump_cert(cert: x509.Certificate) -> bytes: + return cert.public_bytes(encoding) - # assumes that OpenSSL.crypto.dump_certificate includes ending + # assumes that x509.Certificate.public_bytes includes ending # newline character return b"".join(_dump_cert(cert) for cert in chain) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-3.3.0/acme/messages.py new/acme-4.0.0/acme/messages.py --- old/acme-3.3.0/acme/messages.py 2025-03-11 16:03:28.000000000 +0100 +++ new/acme-4.0.0/acme/messages.py 2025-04-08 00:03:33.000000000 +0200 @@ -12,7 +12,8 @@ from typing import Tuple from typing import Type from typing import TypeVar -import warnings + +from cryptography import x509 import josepy as jose @@ -580,45 +581,33 @@ class CertificateRequest(jose.JSONObjectWithFields): """ACME newOrder request. - :ivar jose.ComparableX509 csr: - `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` + :ivar x509.CertificateSigningRequest csr: `x509.CertificateSigningRequest` """ - with warnings.catch_warnings(): - warnings.filterwarnings('ignore', - message='The next major version of josepy will remove josepy.util.ComparableX509') - csr: jose.ComparableX509 = jose.field( - 'csr', decoder=jose.decode_csr, encoder=jose.encode_csr) + csr: x509.CertificateSigningRequest = jose.field( + 'csr', decoder=jose.decode_csr, encoder=jose.encode_csr) class CertificateResource(ResourceWithURI): """Certificate Resource. - :ivar josepy.util.ComparableX509 body: - `OpenSSL.crypto.X509` wrapped in `.ComparableX509` + :ivar x509.Certificate body: `x509.Certificate` :ivar str cert_chain_uri: URI found in the 'up' ``Link`` header :ivar tuple authzrs: `tuple` of `AuthorizationResource`. """ - with warnings.catch_warnings(): - warnings.filterwarnings('ignore', - message='The next major version of josepy will remove josepy.util.ComparableX509') - cert_chain_uri: str = jose.field('cert_chain_uri') + cert_chain_uri: str = jose.field('cert_chain_uri') authzrs: Tuple[AuthorizationResource, ...] = jose.field('authzrs') class Revocation(jose.JSONObjectWithFields): """Revocation message. - :ivar jose.ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in - `jose.ComparableX509` + :ivar x509.Certificate certificate: `x509.Certificate` """ - with warnings.catch_warnings(): - warnings.filterwarnings('ignore', - message='The next major version of josepy will remove josepy.util.ComparableX509') - certificate: jose.ComparableX509 = jose.field( - 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) + certificate: x509.Certificate = jose.field( + 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) reason: int = jose.field('reason') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-3.3.0/acme.egg-info/PKG-INFO new/acme-4.0.0/acme.egg-info/PKG-INFO --- old/acme-3.3.0/acme.egg-info/PKG-INFO 2025-03-11 16:03:30.000000000 +0100 +++ new/acme-4.0.0/acme.egg-info/PKG-INFO 2025-04-08 00:03:34.000000000 +0200 @@ -1,6 +1,6 @@ -Metadata-Version: 2.2 +Metadata-Version: 2.4 Name: acme -Version: 3.3.0 +Version: 4.0.0 Summary: ACME protocol implementation in Python Home-page: https://github.com/certbot/certbot Author: Certbot Project @@ -21,7 +21,7 @@ Requires-Python: >=3.9 License-File: LICENSE.txt Requires-Dist: cryptography>=43.0.0 -Requires-Dist: josepy<2,>=1.13.0 +Requires-Dist: josepy>=2.0.0 Requires-Dist: PyOpenSSL>=25.0.0 Requires-Dist: pyrfc3339 Requires-Dist: pytz>=2019.3 @@ -38,6 +38,7 @@ Dynamic: classifier Dynamic: home-page Dynamic: license +Dynamic: license-file Dynamic: provides-extra Dynamic: requires-dist Dynamic: requires-python diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-3.3.0/acme.egg-info/requires.txt new/acme-4.0.0/acme.egg-info/requires.txt --- old/acme-3.3.0/acme.egg-info/requires.txt 2025-03-11 16:03:30.000000000 +0100 +++ new/acme-4.0.0/acme.egg-info/requires.txt 2025-04-08 00:03:34.000000000 +0200 @@ -1,5 +1,5 @@ cryptography>=43.0.0 -josepy<2,>=1.13.0 +josepy>=2.0.0 PyOpenSSL>=25.0.0 pyrfc3339 pytz>=2019.3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-3.3.0/examples/http01_example.py new/acme-4.0.0/examples/http01_example.py --- old/acme-3.3.0/examples/http01_example.py 2025-03-11 16:03:28.000000000 +0100 +++ new/acme-4.0.0/examples/http01_example.py 2025-04-08 00:03:33.000000000 +0200 @@ -200,11 +200,7 @@ # Revoke certificate - fullchain_com = jose.ComparableX509( - OpenSSL.crypto.X509.from_cryptography( - x509.load_pem_x509_certificate(fullchain_pem) - ) - ) + fullchain_com = x509.load_pem_x509_certificate(fullchain_pem) try: client_acme.revoke(fullchain_com, 0) # revocation reason = 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-3.3.0/setup.py new/acme-4.0.0/setup.py --- old/acme-3.3.0/setup.py 2025-03-11 16:03:29.000000000 +0100 +++ new/acme-4.0.0/setup.py 2025-04-08 00:03:33.000000000 +0200 @@ -1,15 +1,11 @@ -import sys - from setuptools import find_packages from setuptools import setup -version = '3.3.0' +version = '4.0.0' install_requires = [ 'cryptography>=43.0.0', - # Josepy 2+ may introduce backward incompatible changes by droping usage of - # deprecated PyOpenSSL APIs. - 'josepy>=1.13.0, <2', + 'josepy>=2.0.0', # PyOpenSSL>=25.0.0 is just needed to satisfy mypy right now so this dependency can probably be # relaxed to >=24.0.0 if needed. 'PyOpenSSL>=25.0.0',