Change acme.renewal_time to only check ARI, and not also a default time. Separate out default check and use that in should_autorenew instead. (#10309)

Fixes https://github.com/certbot/certbot/issues/10298. Replacement for
#10301.

---------

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
This commit is contained in:
ohemorange
2025-06-04 14:48:44 -07:00
committed by GitHub
co-authored by Brad Warren
parent 3cbe1288c9
commit 48f34938c6
4 changed files with 104 additions and 30 deletions
+6 -6
View File
@@ -463,21 +463,21 @@ class ClientV2Test(unittest.TestCase):
ClientV2.get_directory('https://example.com/dir', self.net).to_partial_json()
def test_renewal_time_no_renewal_info(self):
# A directory with no 'renewalInfo' should result in default renewal periods.
# A directory with no 'renewalInfo' should result in None.
self.client.directory = messages.Directory({})
cert_pem = make_cert_for_renewal(
not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),
not_after=datetime.datetime(2025, 3, 20, 00, 00, 00),
)
t, _ = self.client.renewal_time(cert_pem)
assert t == datetime.datetime(2025, 3, 16, 00, 00, 00, tzinfo=datetime.timezone.utc)
assert t == None
cert_pem = make_cert_for_renewal(
not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),
not_after=datetime.datetime(2025, 3, 30, 00, 00, 00),
)
t, _ = self.client.renewal_time(cert_pem)
assert t == datetime.datetime(2025, 3, 24, 00, 00, 00, tzinfo=datetime.timezone.utc)
assert t == None
def test_renewal_time_with_renewal_info(self):
from cryptography import x509
@@ -526,7 +526,7 @@ class ClientV2Test(unittest.TestCase):
self.client.directory = messages.Directory({
'renewalInfo': 'https://www.letsencrypt-demo.org/acme/renewal-info',
})
# Failure to fetch the 'renewalInfo' URL should return default timings
# Failure to fetch the 'renewalInfo' URL should return None
self.net.get.side_effect = requests.exceptions.RequestException
cert_pem = make_cert_for_renewal(
@@ -534,14 +534,14 @@ class ClientV2Test(unittest.TestCase):
not_after=datetime.datetime(2025, 3, 20, 00, 00, 00),
)
t, _ = self.client.renewal_time(cert_pem)
assert t == datetime.datetime(2025, 3, 16, 00, 00, 00, tzinfo=datetime.timezone.utc)
assert t == None
cert_pem = make_cert_for_renewal(
not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),
not_after=datetime.datetime(2025, 3, 30, 00, 00, 00),
)
t, _ = self.client.renewal_time(cert_pem)
assert t == datetime.datetime(2025, 3, 24, 00, 00, 00, tzinfo=datetime.timezone.utc)
assert t == None
@mock.patch('acme.client.datetime')
def test_renewal_time_returns_retry_after(self, dt_mock):
+10 -12
View File
@@ -314,7 +314,8 @@ class ClientV2:
raise e
return self.poll_finalization(orderr, deadline, fetch_alternative_chains)
def renewal_time(self, cert_pem: bytes) -> Tuple[datetime.datetime, datetime.datetime]:
def renewal_time(self, cert_pem: bytes
) -> Tuple[Optional[datetime.datetime], datetime.datetime]:
"""Return an appropriate time to attempt renewal of the certificate,
and the next time to ask the ACME server for renewal info.
@@ -322,11 +323,15 @@ class ClientV2:
based on a fetch of the renewal info resource for the certificate
(https://www.ietf.org/archive/id/draft-ietf-acme-ari-08.html).
If there is no "renewalInfo" field, this function will fall back to
reasonable defaults based on the certificate lifetime.
If there is no "renewalInfo" field, this function will return a tuple of
None, and the next time to ask the ACME server for renewal info.
This function may make other network calls in the future (e.g., OCSP
or CRL).
:param bytes cert_pem: cert as pem file
:returns: Tuple of time to attempt renewal, next time to ask for renewal info
"""
now = datetime.datetime.now()
# https://www.ietf.org/archive/id/draft-ietf-acme-ari-08.html#section-4.3.3
@@ -334,24 +339,17 @@ class ClientV2:
cert = x509.load_pem_x509_certificate(cert_pem)
not_before = cert.not_valid_before_utc
lifetime = cert.not_valid_after_utc - not_before
if lifetime.total_seconds() < 10 * 86400:
default_renewal_time = not_before + lifetime / 2
else:
default_renewal_time = not_before + lifetime * 2 / 3
try:
renewal_info_base_url = self.directory['renewalInfo']
except KeyError:
return default_renewal_time, now + default_retry_after
return None, now + default_retry_after
ari_url = renewal_info_base_url + '/' + _renewal_info_path_component(cert)
try:
resp = self.net.get(ari_url, content_type='application/json')
except (requests.exceptions.RequestException, messages.Error) as error:
logger.warning("failed to fetch renewal_info URL (%s): %s", ari_url, error)
return default_renewal_time, now + default_retry_after
return None, now + default_retry_after
renewal_info: messages.RenewalInfo = messages.RenewalInfo.from_json(resp.json())