Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-certbot for openSUSE:Factory checked in at 2025-04-22 17:28:57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-certbot (Old) and /work/SRC/openSUSE:Factory/.python-certbot.new.30101 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-certbot" Tue Apr 22 17:28:57 2025 rev:56 rq:1271240 version:4.0.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-certbot/python-certbot.changes 2025-03-21 20:23:39.033825955 +0100 +++ /work/SRC/openSUSE:Factory/.python-certbot.new.30101/python-certbot.changes 2025-04-22 17:29:40.074571675 +0200 @@ -1,0 +2,22 @@ +Tue Apr 22 03:35:34 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: ---- certbot-3.3.0.tar.gz New: ---- certbot-4.0.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-certbot.spec ++++++ --- /var/tmp/diff_new_pack.iPnNNN/_old 2025-04-22 17:29:40.702598055 +0200 +++ /var/tmp/diff_new_pack.iPnNNN/_new 2025-04-22 17:29:40.702598055 +0200 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-certbot -Version: 3.3.0 +Version: 4.0.0 Release: 0 Summary: ACME client License: Apache-2.0 @@ -30,7 +30,7 @@ BuildRequires: %{python_module cryptography >= 43.0.0} BuildRequires: %{python_module distro >= 1.0.1} BuildRequires: %{python_module importlib-metadata if %python-base < 3.10} -BuildRequires: %{python_module josepy >= 1.13.0} +BuildRequires: %{python_module josepy >= 2.0.0} BuildRequires: %{python_module parsedatetime >= 2.4} BuildRequires: %{python_module pip} BuildRequires: %{python_module pyRFC3339} @@ -44,7 +44,7 @@ Requires: python-configobj >= 5.0.6 Requires: python-cryptography >= 43.0.0 Requires: python-distro >= 1.0.1 -Requires: python-josepy >= 1.9.0 +Requires: python-josepy >= 2.0.0 Requires: python-parsedatetime >= 2.4 Requires: python-pyRFC3339 Requires: python-pytz >= 2019.3 @@ -63,8 +63,7 @@ to lower the barriers to entry for encrypting all HTTP traffic on the internet. %prep -%setup -q -n certbot-%{version} -%autopatch -p1 +%autosetup -p1 -n certbot-%{version} %build %pyproject_wheel @@ -94,6 +93,6 @@ %license LICENSE.txt %doc README.rst %{python_sitelib}/certbot -%{python_sitelib}/certbot-%{version}*info +%{python_sitelib}/certbot-%{version}.dist-info %python_alternative %{_bindir}/certbot ++++++ certbot-3.3.0.tar.gz -> certbot-4.0.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/CHANGELOG.md new/certbot-4.0.0/CHANGELOG.md --- old/certbot-3.3.0/CHANGELOG.md 2025-03-11 16:03:28.000000000 +0100 +++ new/certbot-4.0.0/CHANGELOG.md 2025-04-08 00:03:33.000000000 +0200 @@ -2,6 +2,36 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). +## 4.0.0 - 2025-04-07 + +### Added + +* The --preferred-profile and --required-profile flags allow requesting a profile. + https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/ + +### Changed + +* Certificates now renew with 1/3rd of lifetime left (or 1/2 of lifetime left, + if the lifetime is shorter than 10 days). This is a change from a hardcoded + renewal at 30 days before expiration. The config field renew_before_expiry + still overrides this default. + +* 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. + +More details about these changes can be found on our GitHub repo. + ## 3.3.0 - 2025-03-11 ### Added @@ -12,9 +42,9 @@ * The --register-unsafely-without-email flag is no longer needed in non-interactive mode. * In interactive mode, pressing Enter at the email prompt will register without an email. -* deprecated `acme.crypto_util.dump_pyopenssl_chain` * deprecated `acme.crypto_util._pyopenssl_cert_or_req_all_names` * deprecated `acme.crypto_util._pyopenssl_cert_or_req_san` +* deprecated `acme.crypto_util.dump_pyopenssl_chain` * deprecated `certbot.crypto_util.dump_pyopenssl_chain` * deprecated `certbot.crypto_util.pyopenssl_load_certificate` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/PKG-INFO new/certbot-4.0.0/PKG-INFO --- old/certbot-3.3.0/PKG-INFO 2025-03-11 16:03:29.509018700 +0100 +++ new/certbot-4.0.0/PKG-INFO 2025-04-08 00:03:34.270558800 +0200 @@ -1,6 +1,6 @@ -Metadata-Version: 2.2 +Metadata-Version: 2.4 Name: certbot -Version: 3.3.0 +Version: 4.0.0 Summary: ACME client Home-page: https://github.com/certbot/certbot Author: Certbot Project @@ -27,13 +27,13 @@ Classifier: Topic :: Utilities Requires-Python: >=3.9 License-File: LICENSE.txt -Requires-Dist: acme>=3.3.0 +Requires-Dist: acme>=4.0.0 Requires-Dist: ConfigArgParse>=1.5.3 Requires-Dist: configobj>=5.0.6 Requires-Dist: cryptography>=43.0.0 Requires-Dist: distro>=1.0.1 Requires-Dist: importlib_metadata>=4.6; python_version < "3.10" -Requires-Dist: josepy<2,>=1.13.0 +Requires-Dist: josepy>=2.0.0 Requires-Dist: parsedatetime>=2.4 Requires-Dist: pyrfc3339 Requires-Dist: pytz>=2019.3 @@ -94,6 +94,7 @@ Dynamic: description 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/certbot-3.3.0/certbot/__init__.py new/certbot-4.0.0/certbot/__init__.py --- old/certbot-3.3.0/certbot/__init__.py 2025-03-11 16:03:29.000000000 +0100 +++ new/certbot-4.0.0/certbot/__init__.py 2025-04-08 00:03:33.000000000 +0200 @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '3.3.0' +__version__ = '4.0.0' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/certbot/_internal/cli/__init__.py new/certbot-4.0.0/certbot/_internal/cli/__init__.py --- old/certbot-3.3.0/certbot/_internal/cli/__init__.py 2025-03-11 16:03:28.000000000 +0100 +++ new/certbot-4.0.0/certbot/_internal/cli/__init__.py 2025-04-08 00:03:33.000000000 +0200 @@ -360,6 +360,16 @@ "user; only needed if your config is somewhere unsafe like /tmp/") helpful.add( [None, "certonly", "renew", "run"], + "--required-profile", dest="required_profile", + default=flag_default("required_profile"), help=config_help("required_profile") + ) + helpful.add( + [None, "certonly", "renew", "run"], + "--preferred-profile", dest="preferred_profile", + default=flag_default("preferred_profile"), help=config_help("preferred_profile") + ) + helpful.add( + [None, "certonly", "renew", "run"], "--preferred-chain", dest="preferred_chain", default=flag_default("preferred_chain"), help=config_help("preferred_chain") ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/certbot/_internal/client.py new/certbot-4.0.0/certbot/_internal/client.py --- old/certbot-3.3.0/certbot/_internal/client.py 2025-03-11 16:03:28.000000000 +0100 +++ new/certbot-4.0.0/certbot/_internal/client.py 2025-04-08 00:03:33.000000000 +0200 @@ -48,8 +48,9 @@ logger = logging.getLogger(__name__) -def acme_from_config_key(config: configuration.NamespaceConfig, key: jose.JWK, - regr: Optional[messages.RegistrationResource] = None +def acme_from_config_key(config: configuration.NamespaceConfig, + key: jose.JWK, + regr: Optional[messages.RegistrationResource] = None, ) -> acme_client.ClientV2: """Wrangle ACME client construction""" if key.typ == 'EC': @@ -470,8 +471,17 @@ """ if not self.acme: raise errors.Error("ACME client is not set.") + + profile = None + available_profiles = self.acme.directory.meta.profiles + preferred_profile = self.config.preferred_profile + if self.config.required_profile is not None: + profile = self.config.required_profile + elif (preferred_profile and available_profiles and + preferred_profile in available_profiles): + profile = preferred_profile try: - orderr = self.acme.new_order(csr_pem) + orderr = self.acme.new_order(csr_pem, profile=profile) except acme_errors.WildcardUnsupportedError: raise errors.Error("The currently selected ACME CA endpoint does" " not support issuing wildcard certificates.") @@ -484,7 +494,7 @@ deactivated, failed = self.auth_handler.deactivate_valid_authorizations(orderr) if deactivated: logger.debug("Recreating order after authz deactivations") - orderr = self.acme.new_order(csr_pem) + orderr = self.acme.new_order(csr_pem, profile=profile) if failed: logger.warning("Certbot was unable to obtain fresh authorizations for every domain" ". The dry run will continue, but results may not be accurate.") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/certbot/_internal/constants.py new/certbot-4.0.0/certbot/_internal/constants.py --- old/certbot-3.3.0/certbot/_internal/constants.py 2025-03-11 16:03:28.000000000 +0100 +++ new/certbot-4.0.0/certbot/_internal/constants.py 2025-04-08 00:03:33.000000000 +0200 @@ -70,6 +70,8 @@ uir=None, # listed as False in help output staple=None, # listed as False in help output strict_permissions=False, + required_profile=None, + preferred_profile=None, preferred_chain=None, pref_challs=[], validate_hooks=True, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/certbot/_internal/main.py new/certbot-4.0.0/certbot/_internal/main.py --- old/certbot-3.3.0/certbot/_internal/main.py 2025-03-11 16:03:28.000000000 +0100 +++ new/certbot-4.0.0/certbot/_internal/main.py 2025-04-08 00:03:33.000000000 +0200 @@ -17,6 +17,7 @@ from typing import Union import configobj +from cryptography import x509 import josepy as jose from josepy import b64 @@ -1364,10 +1365,10 @@ acme = client.acme_from_config_key(config, acc.key, acc.regr) with open(config.cert_path, 'rb') as f: - cert = crypto_util.pyopenssl_load_certificate(f.read())[0] + cert = x509.load_pem_x509_certificate(f.read()) logger.debug("Reason code for revocation: %s", config.reason) try: - acme.revoke(jose.ComparableX509(cert), config.reason) + acme.revoke(cert, config.reason) _delete_if_appropriate(config) except acme_errors.ClientError as e: return str(e) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/certbot/_internal/storage.py new/certbot-4.0.0/certbot/_internal/storage.py --- old/certbot-3.3.0/certbot/_internal/storage.py 2025-03-11 16:03:28.000000000 +0100 +++ new/certbot-4.0.0/certbot/_internal/storage.py 2025-04-08 00:03:33.000000000 +0200 @@ -1001,17 +1001,32 @@ logger.debug("Should renew, certificate is revoked.") return True - # Renews some period before expiry time - default_interval = constants.RENEWER_DEFAULTS["renew_before_expiry"] - interval = self.configuration.get("renew_before_expiry", default_interval) - expiry = crypto_util.notAfter(self.version( - "cert", self.latest_common_version())) + cert = self.version("cert", self.latest_common_version()) + notBefore = crypto_util.notBefore(cert) + notAfter = crypto_util.notAfter(cert) + lifetime = notAfter - notBefore + + config_interval = self.configuration.get("renew_before_expiry") now = datetime.datetime.now(pytz.UTC) - if expiry < add_time_interval(now, interval): + if config_interval is not None and notAfter < add_time_interval(now, config_interval): logger.debug("Should renew, less than %s before certificate " - "expiry %s.", interval, - expiry.strftime("%Y-%m-%d %H:%M:%S %Z")) + "expiry %s.", config_interval, + notAfter.strftime("%Y-%m-%d %H:%M:%S %Z")) + return True + + # No config for "renew_before_expiry", provide default behavior. + # For most certs, renew with 1/3 of certificate lifetime remaining. + # For short lived certificates, renew at 1/2 of certificate lifetime. + default_interval = lifetime / 3 + if lifetime.total_seconds() < 10 * 86400: + default_interval = lifetime / 2 + remaining_time = notAfter - now + if remaining_time < default_interval: + logger.debug("Should renew, less than %ss before certificate " + "expiry %s.", default_interval, + notAfter.strftime("%Y-%m-%d %H:%M:%S %Z")) return True + return False @classmethod @@ -1091,8 +1106,7 @@ logger.debug("Writing chain to %s.", target["chain"]) f_b.write(chain) with open(target["fullchain"], "wb") as f_b: - # assumes that OpenSSL.crypto.dump_certificate includes - # ending newline character + # assumes the cert includes ending newline character logger.debug("Writing full chain to %s.", target["fullchain"]) f_b.write(cert + chain) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/certbot/_internal/tests/client_test.py new/certbot-4.0.0/certbot/_internal/tests/client_test.py --- old/certbot-3.3.0/certbot/_internal/tests/client_test.py 2025-03-11 16:03:28.000000000 +0100 +++ new/certbot-4.0.0/certbot/_internal/tests/client_test.py 2025-04-08 00:03:33.000000000 +0200 @@ -383,6 +383,92 @@ self.eg_order.fullchain_pem) @mock.patch("certbot._internal.client.crypto_util") + def test_obtain_certificate_no_profile_preference(self, mock_crypto_util): + csr = util.CSR(form="pem", file=None, data=CSR_SAN) + mock_crypto_util.generate_csr.return_value = csr + mock_crypto_util.generate_key.return_value = mock.sentinel.key + self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain) + + self.client.config.required_profile = None + self.client.config.preferred_profile = None + + self._test_obtain_certificate_common(mock.sentinel.key, csr) + self.acme.new_order.assert_called_once_with(mock.ANY, profile=None) + + @mock.patch("certbot._internal.client.crypto_util") + def test_obtain_certificate_required_profile(self, mock_crypto_util): + csr = util.CSR(form="pem", file=None, data=CSR_SAN) + mock_crypto_util.generate_csr.return_value = csr + mock_crypto_util.generate_key.return_value = mock.sentinel.key + self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain) + + self.client.config.required_profile = "exampleProfile" + self.client.config.preferred_profile = None + + self._test_obtain_certificate_common(mock.sentinel.key, csr) + self.acme.new_order.assert_called_once_with(mock.ANY, profile="exampleProfile") + + @mock.patch("certbot._internal.client.crypto_util") + def test_obtain_certificate_preferred_profile_exists(self, mock_crypto_util): + csr = util.CSR(form="pem", file=None, data=CSR_SAN) + mock_crypto_util.generate_csr.return_value = csr + mock_crypto_util.generate_key.return_value = mock.sentinel.key + self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain) + + self.client.config.required_profile = None + self.client.config.preferred_profile = "exampleProfile" + + from acme.messages import Directory + self.acme.directory = Directory.from_json({ + 'meta': { + 'profiles': { + 'exampleProfile': 'here is some descriptive text, very informative', + } + } + }) + + self._test_obtain_certificate_common(mock.sentinel.key, csr) + self.acme.new_order.assert_called_once_with(mock.ANY, profile="exampleProfile") + + @mock.patch("certbot._internal.client.crypto_util") + def test_obtain_certificate_preferred_profile_does_not_exist(self, mock_crypto_util): + csr = util.CSR(form="pem", file=None, data=CSR_SAN) + mock_crypto_util.generate_csr.return_value = csr + mock_crypto_util.generate_key.return_value = mock.sentinel.key + self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain) + + self.client.config.required_profile = None + self.client.config.preferred_profile = "thisProfileDoesNotExist" + + from acme.messages import Directory + self.acme.directory = Directory.from_json({ + 'meta': { + 'profiles': { + 'example': 'profiles!', + } + } + }) + + self._test_obtain_certificate_common(mock.sentinel.key, csr) + self.acme.new_order.assert_called_once_with(mock.ANY, profile=None) + + @mock.patch("certbot._internal.client.crypto_util") + def test_obtain_certificate_preferred_profile_no_profiles_exist(self, mock_crypto_util): + csr = util.CSR(form="pem", file=None, data=CSR_SAN) + mock_crypto_util.generate_csr.return_value = csr + mock_crypto_util.generate_key.return_value = mock.sentinel.key + self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain) + + self.client.config.required_profile = None + self.client.config.preferred_profile = "thisProfileDoesNotExist" + + from acme.messages import Directory + self.acme.directory = Directory.from_json({}) + + self._test_obtain_certificate_common(mock.sentinel.key, csr) + self.acme.new_order.assert_called_once_with(mock.ANY, profile=None) + + @mock.patch("certbot._internal.client.crypto_util") def test_obtain_certificate_partial_success(self, mock_crypto_util): csr = util.CSR(form="pem", file=mock.sentinel.csr_file, data=CSR_SAN) key = util.CSR(form="pem", file=mock.sentinel.key_file, data=CSR_SAN) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/certbot/_internal/tests/crypto_util_test.py new/certbot-4.0.0/certbot/_internal/tests/crypto_util_test.py --- old/certbot-3.3.0/certbot/_internal/tests/crypto_util_test.py 2025-03-11 16:03:28.000000000 +0100 +++ new/certbot-4.0.0/certbot/_internal/tests/crypto_util_test.py 2025-04-08 00:03:33.000000000 +0200 @@ -1,19 +1,17 @@ """Tests for certbot.crypto_util.""" -import binascii import logging import re import sys import unittest from unittest import mock -import warnings -import OpenSSL import pytest from cryptography import x509 -from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.serialization import Encoding +from acme import crypto_util as acme_crypto_util from certbot import errors from certbot import util from certbot.compat import filesystem @@ -143,7 +141,7 @@ data = test_util.load_vector('csr_512.der') data_pem = test_util.load_vector('csr_512.pem') - assert (OpenSSL.crypto.FILETYPE_PEM, + assert (acme_crypto_util.Format.PEM, util.CSR(file=csrfile, data=data_pem, form="pem"), @@ -154,7 +152,7 @@ csrfile = test_util.vector_path('csr_512.pem') data = test_util.load_vector('csr_512.pem') - assert (OpenSSL.crypto.FILETYPE_PEM, + assert (acme_crypto_util.Format.PEM, util.CSR(file=csrfile, data=data, form="pem"), @@ -396,39 +394,8 @@ self._call(test_util.load_vector('csr-6sans_512.pem')) def test_der(self): - from OpenSSL.crypto import FILETYPE_ASN1 assert ['Example.com'] == \ - self._call(test_util.load_vector('csr_512.der'), typ=FILETYPE_ASN1) - - -class CertLoaderTest(unittest.TestCase): - """Tests for certbot.crypto_util.pyopenssl_load_certificate""" - - def test_load_valid_cert(self): - from certbot.crypto_util import pyopenssl_load_certificate - - with warnings.catch_warnings(): - warnings.filterwarnings( - 'ignore', - message='certbot.crypto_util.pyopenssl_load_certificate is *' - ) - cert, file_type = pyopenssl_load_certificate(CERT) - - assert file_type == OpenSSL.crypto.FILETYPE_PEM - assert binascii.unhexlify( - cert.digest("sha256").replace(b":", b"") - ) == x509.load_pem_x509_certificate(CERT).fingerprint(hashes.SHA256()) - - def test_load_invalid_cert(self): - from certbot.crypto_util import pyopenssl_load_certificate - bad_cert_data = CERT.replace(b"BEGIN CERTIFICATE", b"ASDFASDFASDF!!!") - with pytest.raises(errors.Error): - with warnings.catch_warnings(): - warnings.filterwarnings( - 'ignore', - message='certbot.crypto_util.pyopenssl_load_certificate is *' - ) - pyopenssl_load_certificate(bad_cert_data) + self._call(test_util.load_vector('csr_512.der'), typ=acme_crypto_util.Format.DER) class NotBeforeTest(unittest.TestCase): @@ -460,20 +427,19 @@ class CertAndChainFromFullchainTest(unittest.TestCase): """Tests for certbot.crypto_util.cert_and_chain_from_fullchain""" - def _parse_and_reencode_pem(self, cert_pem): - from OpenSSL import crypto - return crypto.dump_certificate(crypto.FILETYPE_PEM, - crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)).decode() + def _parse_and_reencode_pem(self, cert_pem:str)->str: + cert = x509.load_pem_x509_certificate(cert_pem.encode()) + return cert.public_bytes(Encoding.PEM).decode() def test_cert_and_chain_from_fullchain(self): - cert_pem = CERT.decode() - chain_pem = cert_pem + SS_CERT.decode() - fullchain_pem = cert_pem + chain_pem - spacey_fullchain_pem = cert_pem + u'\n' + chain_pem - crlf_fullchain_pem = fullchain_pem.replace(u'\n', u'\r\n') + cert_pem: str = CERT.decode() + chain_pem: str = cert_pem + SS_CERT.decode() + fullchain_pem: str = cert_pem + chain_pem + spacey_fullchain_pem: str = cert_pem + u'\n' + chain_pem + crlf_fullchain_pem: str = fullchain_pem.replace(u'\n', u'\r\n') # In the ACME v1 code path, the fullchain is constructed by loading cert+chain DERs - # and using OpenSSL to dump them, so here we confirm that OpenSSL is producing certs + # and using OpenSSL to dump them, so here we confirm that cryptography is producing certs # that will be parseable by cert_and_chain_from_fullchain. acmev1_fullchain_pem = self._parse_and_reencode_pem(cert_pem) + \ self._parse_and_reencode_pem(cert_pem) + self._parse_and_reencode_pem(SS_CERT.decode()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/certbot/_internal/tests/main_test.py new/certbot-4.0.0/certbot/_internal/tests/main_test.py --- old/certbot-3.3.0/certbot/_internal/tests/main_test.py 2025-03-11 16:03:28.000000000 +0100 +++ new/certbot-4.0.0/certbot/_internal/tests/main_test.py 2025-04-08 00:03:33.000000000 +0200 @@ -14,15 +14,14 @@ from typing import List import unittest from unittest import mock -import warnings import configobj +from cryptography import x509 import josepy as jose import pytest import pytz from acme.messages import Error as acme_error -from certbot import crypto_util from certbot import errors from certbot import interfaces from certbot import util @@ -219,6 +218,7 @@ with pytest.raises(errors.NotSupportedError): main.run(self.config, plugins) + class CertonlyTest(unittest.TestCase): """Tests for certbot._internal.main.certonly.""" @@ -436,12 +436,7 @@ config = cli.prepare_and_parse_args(plugins, args) from certbot._internal.main import revoke - with warnings.catch_warnings(): - warnings.filterwarnings( - 'ignore', - message='certbot.crypto_util.pyopenssl_load_certificate is *' - ) - revoke(config, plugins) + revoke(config, plugins) @mock.patch('certbot._internal.main._delete_if_appropriate') @mock.patch('certbot._internal.main.client.acme_client') @@ -1801,23 +1796,16 @@ mock_delete_if_appropriate): mock_delete_if_appropriate.return_value = False server = 'foo.bar' - with warnings.catch_warnings(): - warnings.filterwarnings( - 'ignore', - message='certbot.crypto_util.pyopenssl_load_certificate is *' - ) - self._call_no_clientmock(['--cert-path', SS_CERT_PATH, '--key-path', RSA2048_KEY_PATH, - '--server', server, 'revoke']) - with open(RSA2048_KEY_PATH, 'rb') as f: - assert mock_acme_client.ClientV2.call_count == 1 - assert mock_acme_client.ClientNetwork.call_args[0][0] == \ - jose.JWK.load(f.read()) - with open(SS_CERT_PATH, 'rb') as f: - cert = crypto_util.pyopenssl_load_certificate(f.read())[0] - mock_revoke = mock_acme_client.ClientV2().revoke - mock_revoke.assert_called_once_with( - jose.ComparableX509(cert), - mock.ANY) + self._call_no_clientmock(['--cert-path', SS_CERT_PATH, '--key-path', RSA2048_KEY_PATH, + '--server', server, 'revoke']) + with open(RSA2048_KEY_PATH, 'rb') as f: + assert mock_acme_client.ClientV2.call_count == 1 + assert mock_acme_client.ClientNetwork.call_args[0][0] == \ + jose.JWK.load(f.read()) + with open(SS_CERT_PATH, 'rb') as f: + cert = x509.load_pem_x509_certificate(f.read()) + mock_revoke = mock_acme_client.ClientV2().revoke + mock_revoke.assert_called_once_with(cert, mock.ANY) def test_revoke_with_key_mismatch(self): server = 'foo.bar' @@ -1831,18 +1819,11 @@ mock_delete_if_appropriate): mock_delete_if_appropriate.return_value = False mock_determine_account.return_value = (mock.MagicMock(), None) - with warnings.catch_warnings(): - warnings.filterwarnings( - 'ignore', - message='certbot.crypto_util.pyopenssl_load_certificate is *' - ) - _, _, _, client = self._call(['--cert-path', CERT, 'revoke']) - with open(CERT) as f: - cert = crypto_util.pyopenssl_load_certificate(f.read())[0] - mock_revoke = client.acme_from_config_key().revoke - mock_revoke.assert_called_once_with( - jose.ComparableX509(cert), - mock.ANY) + _, _, _, client = self._call(['--cert-path', CERT, 'revoke']) + with open(CERT, 'rb') as f: + cert = x509.load_pem_x509_certificate(f.read()) + mock_revoke = client.acme_from_config_key().revoke + mock_revoke.assert_called_once_with(cert, mock.ANY) @mock.patch('certbot._internal.log.post_arg_parse_setup') def test_register(self, _): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/certbot/_internal/tests/storage_test.py new/certbot-4.0.0/certbot/_internal/tests/storage_test.py --- old/certbot-3.3.0/certbot/_internal/tests/storage_test.py 2025-03-11 16:03:28.000000000 +0100 +++ new/certbot-4.0.0/certbot/_internal/tests/storage_test.py 2025-04-08 00:03:33.000000000 +0200 @@ -19,8 +19,33 @@ from certbot.compat import os import certbot.tests.util as test_util -CERT = test_util.load_cert('cert_512.pem') - +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import serialization, hashes +from cryptography import x509 +from cryptography.x509 import Certificate + +import datetime +from typing import Optional, Any + +def make_cert_with_lifetime(not_before: datetime.datetime, lifetime_days: int) -> bytes: + """Return PEM of a self-signed certificate with the given notBefore and lifetime.""" + key = ec.generate_private_key(ec.SECP256R1()) + not_after=not_before + datetime.timedelta(days=lifetime_days) + cert = x509.CertificateBuilder( + issuer_name=x509.Name([]), + subject_name=x509.Name([]), + public_key=key.public_key(), + serial_number=x509.random_serial_number(), + not_valid_before=not_before, + not_valid_after=not_after, + ).add_extension( + x509.SubjectAlternativeName([x509.DNSName("example.com")]), + critical=False, + ).sign( + private_key=key, + algorithm=hashes.SHA256(), + ) + return cert.public_bytes(serialization.Encoding.PEM) def unlink_all(rc_object): """Unlink all four items associated with this RenewableCert.""" @@ -445,32 +470,45 @@ @mock.patch("certbot._internal.storage.datetime") def test_time_interval_judgments(self, mock_datetime, mock_set_by_user): """Test should_autorenew() on the basis of expiry time windows.""" - test_cert = test_util.load_vector("cert_512.pem") + # Note: this certificate happens to have a lifetime of 7 days, + # and the tests below that use a "None" interval (i.e. choose a + # default) rely on that fact. + # + # Not Before: Dec 11 22:34:45 2014 GMT + # Not After : Dec 18 22:34:45 2014 GMT + not_before = datetime.datetime(2014, 12, 11, 22, 34, 45) + short_cert = make_cert_with_lifetime(not_before, 7) self._write_out_ex_kinds() self.test_rc.update_all_links_to(12) with open(self.test_rc.cert, "wb") as f: - f.write(test_cert) + f.write(short_cert) self.test_rc.update_all_links_to(11) with open(self.test_rc.cert, "wb") as f: - f.write(test_cert) + f.write(short_cert) mock_datetime.timedelta = datetime.timedelta mock_set_by_user.return_value = False self.test_rc.configuration["renewalparams"] = {} for (current_time, interval, result) in [ - # 2014-12-13 12:00:00+00:00 (about 5 days prior to expiry) + # 2014-12-13 12:00 (about 5 days prior to expiry) # Times that should result in autorenewal/autodeployment (1418472000, "2 months", True), (1418472000, "1 week", True), - # Times that should not + # With the "default" logic, this 7-day certificate should autorenew + # at 3.5 days prior to expiry. We haven't reached that yet, + # so don't renew. + (1418472000, None, False), + # 2014-12-16 03:20, a little less than 3.5 days to expiry. + (1418700000, None, True), + # Times that should not renew (1418472000, "4 days", False), (1418472000, "2 days", False), # 2009-05-01 12:00:00+00:00 (about 5 years prior to expiry) # Times that should result in autorenewal/autodeployment (1241179200, "7 years", True), (1241179200, "11 years 2 months", True), - # Times that should not + # Times that should not renew (1241179200, "8 hours", False), (1241179200, "2 days", False), (1241179200, "40 days", False), (1241179200, "9 months", False), # 2015-01-01 (after expiry has already happened, so all @@ -480,6 +518,28 @@ (1420070400, "10 minutes", True), (1420070400, "10 weeks", True), (1420070400, "10 months", True), (1420070400, "10 years", True), (1420070400, "99 months", True), + (1420070400, None, True) + ]: + sometime = datetime.datetime.fromtimestamp(current_time, pytz.UTC) + mock_datetime.datetime.now.return_value = sometime + self.test_rc.configuration["renew_before_expiry"] = interval + assert self.test_rc.should_autorenew() == result + + # Lifetime: 31 years + # Default renewal: about 10 years from expiry + # Not Before: May 29 07:42:01 2017 GMT + # Not After : Mar 30 07:42:01 2048 GMT + not_before=datetime.datetime(2017, 5, 29, 7, 42, 1) + long_cert = make_cert_with_lifetime(not_before, 31 * 365) + self.test_rc.update_all_links_to(12) + with open(self.test_rc.cert, "wb") as f: + f.write(long_cert) + self.test_rc.update_all_links_to(11) + with open(self.test_rc.cert, "wb") as f: + f.write(long_cert) + for (current_time, result) in [ + (2114380800, False), # 2037-01-01 + (2148000000, True), # 2038-01-25 ]: sometime = datetime.datetime.fromtimestamp(current_time, pytz.UTC) mock_datetime.datetime.now.return_value = sometime diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/certbot/configuration.py new/certbot-4.0.0/certbot/configuration.py --- old/certbot-3.3.0/certbot/configuration.py 2025-03-11 16:03:28.000000000 +0100 +++ new/certbot-4.0.0/certbot/configuration.py 2025-04-08 00:03:33.000000000 +0200 @@ -364,6 +364,28 @@ return self.namespace.disable_renew_updates @property + def required_profile(self) -> Optional[str]: + """Request the given profile name from the ACME server. + + If the ACME server returns an error, issuance (or renewal) will fail. + For long-term reliability, setting preferred_profile instead may be + preferable because it allows fallback to a default. Use this setting + when renewal failure is preferable to fallback. + """ + return self.namespace.required_profile + + @property + def preferred_profile(self) -> Optional[str]: + """Request the given profile name from the ACME server, or fallback to default. + + If the given profile name exists in the ACME directory, use it to request a + a certificate. Otherwise, fall back to requesting a certificate without a profile + (which means the CA will use its default profile). This allows renewals to + succeed even if the CA deprecates and removes a given profile. + """ + return self.namespace.preferred_profile + + @property def preferred_chain(self) -> Optional[str]: """Set the preferred certificate chain. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/certbot/crypto_util.py new/certbot-4.0.0/certbot/crypto_util.py --- old/certbot-3.3.0/certbot/crypto_util.py 2025-03-11 16:03:28.000000000 +0100 +++ new/certbot-4.0.0/certbot/crypto_util.py 2025-04-08 00:03:33.000000000 +0200 @@ -14,7 +14,6 @@ from typing import Tuple from typing import TYPE_CHECKING from typing import Union -import warnings from cryptography import x509 from cryptography.exceptions import InvalidSignature @@ -32,8 +31,6 @@ from cryptography.hazmat.primitives.serialization import Encoding from cryptography.hazmat.primitives.serialization import NoEncryption from cryptography.hazmat.primitives.serialization import PrivateFormat -import josepy -from OpenSSL import crypto from OpenSSL import SSL from acme import crypto_util as acme_crypto_util @@ -351,7 +348,7 @@ :raises errors.Error: If they don't match. """ try: - context = SSL.Context(SSL.SSLv23_METHOD) + context = SSL.Context(SSL.TLS_METHOD) context.use_certificate_file(cert_path) context.use_privatekey_file(key_path) context.check_privatekey() @@ -391,32 +388,6 @@ raise e -def pyopenssl_load_certificate(data: bytes) -> Tuple[crypto.X509, int]: - """Load PEM/DER certificate. - - :raises errors.Error: - - Deprecated - .. deprecated: 3.2.1 - """ - warnings.warn( - "certbot.crypto_util.pyopenssl_load_certificate is deprecated and " - "will be removed in the next major release of Certbot.", - DeprecationWarning, - stacklevel=2 - ) - - openssl_errors = [] - - for file_type in (crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1): - try: - return crypto.load_certificate(file_type, data), file_type - except crypto.Error as error: # TODO: other errors? - openssl_errors.append(error) - raise errors.Error("Unable to load: {0}".format(",".join( - str(error) for error in openssl_errors))) - - def get_sans_from_cert( cert: bytes, typ: Union[acme_crypto_util.Format, int] = acme_crypto_util.Format.PEM ) -> List[str]: @@ -491,29 +462,6 @@ ) -def dump_pyopenssl_chain( - chain: Union[List[crypto.X509], List[josepy.ComparableX509]], - filetype: Union[acme_crypto_util.Format, int] = acme_crypto_util.Format.PEM, -) -> bytes: - """Dump certificate chain into a bundle. - - :param list chain: List of `crypto.X509` (or wrapped in - :class:`josepy.util.ComparableX509`). - - Deprecated - .. deprecated: 3.2.1 - """ - warnings.warn( - "certbot.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... - return acme_crypto_util.dump_pyopenssl_chain(chain, filetype) - - def notBefore(cert_path: str) -> datetime.datetime: """When does the cert at cert_path start being valid? diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/certbot/tests/util.py new/certbot-4.0.0/certbot/tests/util.py --- old/certbot-3.3.0/certbot/tests/util.py 2025-03-11 16:03:28.000000000 +0100 +++ new/certbot-4.0.0/certbot/tests/util.py 2025-04-08 00:03:33.000000000 +0200 @@ -22,6 +22,7 @@ import unittest from unittest import mock +from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey @@ -98,7 +99,7 @@ return data -def _guess_loader(filename: str, loader_pem: int, loader_der: int) -> int: +def _guess_loader(filename: str, loader_pem: Callable, loader_der: Callable) -> Callable: _, ext = os.path.splitext(filename) if ext.lower() == '.pem': return loader_pem @@ -107,23 +108,12 @@ raise ValueError("Loader could not be recognized based on extension") # pragma: no cover -def load_cert(*names: str) -> crypto.X509: +def load_cert(*names: str) -> x509.Certificate: """Load certificate.""" loader = _guess_loader( - names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) - return crypto.load_certificate(loader, load_vector(*names)) - - -def load_csr(*names: str) -> crypto.X509Req: - """Load certificate request.""" - loader = _guess_loader( - names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) - return crypto.load_certificate_request(loader, load_vector(*names)) - - -def load_comparable_csr(*names: str) -> jose.ComparableX509: - """Load ComparableX509 certificate request.""" - return jose.ComparableX509(load_csr(*names)) + names[-1], x509.load_pem_x509_certificate, x509.load_der_x509_certificate + ) + return loader(load_vector(*names)) def load_jose_rsa_private_key_pem(*names: str) -> jose.ComparableRSAKey: @@ -131,9 +121,19 @@ return jose.ComparableRSAKey(load_rsa_private_key_pem(*names)) +def _guess_loader_pyopenssl(filename: str, loader_pem: int, loader_der: int) -> int: + # note: used by `load_rsa_private_key_pem` + _, ext = os.path.splitext(filename) + if ext.lower() == '.pem': + return loader_pem + elif ext.lower() == '.der': + return loader_der + raise ValueError("Loader could not be recognized based on extension") # pragma: no cover + + def load_rsa_private_key_pem(*names: str) -> RSAPrivateKey: """Load RSA private key.""" - loader = _guess_loader(names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) + loader = _guess_loader_pyopenssl(names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) loader_fn: Callable[..., Any] if loader == crypto.FILETYPE_PEM: loader_fn = serialization.load_pem_private_key diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/certbot.egg-info/PKG-INFO new/certbot-4.0.0/certbot.egg-info/PKG-INFO --- old/certbot-3.3.0/certbot.egg-info/PKG-INFO 2025-03-11 16:03:29.000000000 +0100 +++ new/certbot-4.0.0/certbot.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: certbot -Version: 3.3.0 +Version: 4.0.0 Summary: ACME client Home-page: https://github.com/certbot/certbot Author: Certbot Project @@ -27,13 +27,13 @@ Classifier: Topic :: Utilities Requires-Python: >=3.9 License-File: LICENSE.txt -Requires-Dist: acme>=3.3.0 +Requires-Dist: acme>=4.0.0 Requires-Dist: ConfigArgParse>=1.5.3 Requires-Dist: configobj>=5.0.6 Requires-Dist: cryptography>=43.0.0 Requires-Dist: distro>=1.0.1 Requires-Dist: importlib_metadata>=4.6; python_version < "3.10" -Requires-Dist: josepy<2,>=1.13.0 +Requires-Dist: josepy>=2.0.0 Requires-Dist: parsedatetime>=2.4 Requires-Dist: pyrfc3339 Requires-Dist: pytz>=2019.3 @@ -94,6 +94,7 @@ Dynamic: description 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/certbot-3.3.0/certbot.egg-info/requires.txt new/certbot-4.0.0/certbot.egg-info/requires.txt --- old/certbot-3.3.0/certbot.egg-info/requires.txt 2025-03-11 16:03:29.000000000 +0100 +++ new/certbot-4.0.0/certbot.egg-info/requires.txt 2025-04-08 00:03:34.000000000 +0200 @@ -1,9 +1,9 @@ -acme>=3.3.0 +acme>=4.0.0 ConfigArgParse>=1.5.3 configobj>=5.0.6 cryptography>=43.0.0 distro>=1.0.1 -josepy<2,>=1.13.0 +josepy>=2.0.0 parsedatetime>=2.4 pyrfc3339 pytz>=2019.3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/docs/cli-help.txt new/certbot-4.0.0/docs/cli-help.txt --- old/certbot-3.3.0/docs/cli-help.txt 2025-03-11 16:03:28.000000000 +0100 +++ new/certbot-4.0.0/docs/cli-help.txt 2025-04-08 00:03:33.000000000 +0200 @@ -122,7 +122,7 @@ case, and to know when to deprecate support for past Python versions and flags. If you wish to hide this information from the Let's Encrypt server, set this to - "". (default: CertbotACMEClient/3.2.0 (certbot; + "". (default: CertbotACMEClient/3.3.0 (certbot; OS_NAME OS_VERSION) Authenticator/XXX Installer/YYY (SUBCOMMAND; flags: FLAGS) Py/major.minor.patchlevel). The flags encoded in the user agent are: --duplicate, @@ -398,14 +398,6 @@ register: Options for account registration - --register-unsafely-without-email - Specifying this flag enables registering an account - with no email address. This is strongly discouraged, - because you will be unable to receive notice about - impending expiration or revocation of your - certificates or problems with your Certbot - installation that will lead to failure to renew. - (default: False) -m EMAIL, --email EMAIL Email used for registration and recovery contact. Use comma to register multiple emails, ex: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/docs/using.rst new/certbot-4.0.0/docs/using.rst --- old/certbot-3.3.0/docs/using.rst 2025-03-11 16:03:28.000000000 +0100 +++ new/certbot-4.0.0/docs/using.rst 2025-04-08 00:03:33.000000000 +0200 @@ -628,10 +628,6 @@ Renewing certificates --------------------- -.. note:: Let's Encrypt CA issues short-lived certificates (90 - days). Make sure you renew the certificates at least once in 3 - months. - .. seealso:: Most Certbot installations come with automatic renewal out of the box. See `Automated Renewals`_ for more details. @@ -639,14 +635,18 @@ will not renew automatically, unless combined with authentication hook scripts. See `Renewal with the manual plugin <#manual-renewal>`_. -As of version 0.10.0, Certbot supports a ``renew`` action to check -all installed certificates for impending expiry and attempt to renew -them. The simplest form is simply +Certbot supports a ``renew`` action to check all installed certificates for +impending expiry and attempt to renew them. The simplest form is simply ``certbot renew`` -This command attempts to renew any previously-obtained certificates that -expire in less than 30 days. The same plugin and options that were used +This command attempts to renew any previously-obtained certificates which are ready +for renewal. As of Certbot 4.0.0, a certificate is considered ready for renewal +when less than 1/3rd of its lifetime remains. For certificates with a lifetime +of 10 days or less, that threshold is 1/2 of the lifetime. Prior to Certbot 4.0.0 +the threshold was a fixed 30 days. + +The same plugin and options that were used at the time the certificate was originally issued will be used for the renewal attempt, unless you specify other plugins or options. Unlike ``certonly``, ``renew`` acts on multiple certificates and always takes into account whether each one is near diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-3.3.0/setup.py new/certbot-4.0.0/setup.py --- old/certbot-3.3.0/setup.py 2025-03-11 16:03:28.000000000 +0100 +++ new/certbot-4.0.0/setup.py 2025-04-08 00:03:33.000000000 +0200 @@ -5,6 +5,7 @@ from setuptools import find_packages from setuptools import setup + def read_file(filename, encoding='utf8'): """Read unicode from given file.""" with codecs.open(filename, encoding=encoding) as fd: @@ -33,9 +34,7 @@ 'cryptography>=43.0.0', 'distro>=1.0.1', 'importlib_metadata>=4.6; python_version < "3.10"', - # Josepy 2+ may introduce backward incompatible changes by droping usage of - # deprecated PyOpenSSL APIs. - 'josepy>=1.13.0, <2', + 'josepy>=2.0.0', 'parsedatetime>=2.4', 'pyrfc3339', 'pytz>=2019.3',