[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#1106579: unblock: python-certbot/4.0.0-2



Package: release.debian.org
Severity: normal
X-Debbugs-Cc: python-certbot@packages.debian.org, hlieberman@debian.org
Control: affects -1 + src:python-certbot
User: release.debian.org@packages.debian.org
Usertags: unblock
User: hlieberman@debian.org
Usertags: trixie-certbot

Please unblock package python-certbot

[ Reason ]

The certbot 2.x series is end of life and will not receive further updates or
backports of changes.
(https://github.com/certbot/certbot/wiki/Architectural-Decision-Records-2025#-update-to-certbots-version-policy-and-end-of-life-support-on-previous-major-versions)
By far and away, the primary purpose of certbot is to receive certificates from
Let's Encrypt, and the Let's Encrypt team are planning API changes in 2025 which
will break the issuance of TLS certificates for people using the Certbot 2.x
series.

[ Impact ]

If the unblock is not granted, certbot will suddenly stop working at
some point in the future and users' TLS certificates will expire. Because
certbot tends to be used as a set-it-and-forget-it system, and Let's Encrypt has
recently disabled their email notifications, users' websites and applications
may suddenly be unavailable to users and/or vulnerable to MitM.

[ Tests ]

Certbot's two primary plugins (python-certbot-apache, python-certbot-nginx) and
the main utility (python-certbot) have a test harness which exercises the entire
process of getting a certificate against a test environment. This provides very
high confidence that those packages are still working, and that the libraries
which they depend on (python-josepy, python-acme) are in good health. These
tests pass cleanly on ci.d.n for all three invocations.

The dns plugin packages (python-certbot-dns-*) are substantially less
complicated than the other certbot packages and primarily handle communication
with various companies' API layers. Those are unlikely to have broken because of
the changes to certbot's internals; the primary way in which those packages
break are due to API changes on the providers' ends.

[ Risks ]

Upgrading the packages across major versions comes with risks, certainly, but
there is little in the way of alternative. The changes are too complex for me to
be willing to attempt to backport, and in a security critical application, I am
even more reticent than I normally would be. I recognize the late application
introduces even more risk --- and rightfully, I'm sure no small amount of
annoyance --- but it is where we've ended up.

[ Checklist ]
  [X] all changes are documented in the d/changelog
  [X] I reviewed all changes and I approve them
  [X] attach debdiff against the package in testing

[ Other info ]

This is one of a series of identical unblock requests with only
package names and debdiffs differing.

unblock python-certbot/4.0.0-2
diff -Nru python-certbot-2.11.0/certbot/achallenges.py python-certbot-4.0.0/certbot/achallenges.py
--- python-certbot-2.11.0/certbot/achallenges.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/achallenges.py	2025-04-07 18:03:33.000000000 -0400
@@ -19,6 +19,7 @@
 """
 import logging
 from typing import Any
+from typing import Tuple
 from typing import Type
 
 import josepy as jose
@@ -49,7 +50,8 @@
     """Client annotated `KeyAuthorizationChallenge` challenge."""
     __slots__ = ('challb', 'domain', 'account_key') # pylint: disable=redefined-slots-in-subclass
 
-    def response_and_validation(self, *args: Any, **kwargs: Any) -> Any:
+    def response_and_validation(self, *args: Any, **kwargs: Any
+        ) -> Tuple['challenges.KeyAuthorizationChallengeResponse', Any]:
         """Generate response and validation."""
         return self.challb.chall.response_and_validation(
             self.account_key, *args, **kwargs)
diff -Nru python-certbot-2.11.0/certbot/compat/filesystem.py python-certbot-4.0.0/certbot/compat/filesystem.py
--- python-certbot-2.11.0/certbot/compat/filesystem.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/compat/filesystem.py	2025-04-07 18:03:33.000000000 -0400
@@ -5,11 +5,9 @@
 import errno
 import os  # pylint: disable=os-module-forbidden
 import stat
-import sys
 from typing import Any
 from typing import Dict
 from typing import Generator
-from typing import List
 from typing import Optional
 
 try:
@@ -370,27 +368,14 @@
     """
     original_path = file_path
 
-    # Since Python 3.8, os.path.realpath also resolves symlinks on Windows.
-    if POSIX_MODE or sys.version_info >= (3, 8):
-        path = os.path.realpath(file_path)
-        if os.path.islink(path):
-            # If path returned by realpath is still a link, it means that it failed to
-            # resolve the symlink because of a loop.
-            # See realpath code: https://github.com/python/cpython/blob/master/Lib/posixpath.py
-            raise RuntimeError('Error, link {0} is a loop!'.format(original_path))
-        return path
-
-    inspected_paths: List[str] = []
-    while os.path.islink(file_path):
-        link_path = file_path
-        file_path = os.readlink(file_path)
-        if not os.path.isabs(file_path):
-            file_path = os.path.join(os.path.dirname(link_path), file_path)
-        if file_path in inspected_paths:
-            raise RuntimeError('Error, link {0} is a loop!'.format(original_path))
-        inspected_paths.append(file_path)
-
-    return os.path.abspath(file_path)
+    # os.path.realpath also resolves symlinks
+    path = os.path.realpath(file_path)
+    if os.path.islink(path):
+        # If path returned by realpath is still a link, it means that it failed to
+        # resolve the symlink because of a loop.
+        # See realpath code: https://github.com/python/cpython/blob/master/Lib/posixpath.py
+        raise RuntimeError('Error, link {0} is a loop!'.format(original_path))
+    return path
 
 
 def readlink(link_path: str) -> str:
@@ -408,7 +393,7 @@
         return path
 
     # At this point, we know we are on Windows and that the path returned uses
-    # the extended form which is done for all paths in Python 3.8+
+    # the extended form which begins with the prefix \\?\
 
     # Max length of a normal path is 260 characters on Windows, including the non printable
     # termination character "<NUL>". The termination character is not included in Python
diff -Nru python-certbot-2.11.0/certbot/compat/os.py python-certbot-4.0.0/certbot/compat/os.py
--- python-certbot-2.11.0/certbot/compat/os.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/compat/os.py	2025-04-07 18:03:33.000000000 -0400
@@ -161,11 +161,11 @@
                        '(eg. has_min_permissions, has_same_ownership).')
 
 
-# Method os.readlink has a significant behavior change with Python 3.8+. Starting
-# with this version, it will return the resolved path in its "extended-style" form
-# unconditionally, which allows to use more than 259 characters, and its string
-# representation is prepended with "\\?\". Problem is that it does it for any path,
-# and will make equality comparison fail with paths that will use the simple form.
+# On Windows, os.readlink "typically" returns the resolved path in its "extended-style" form which
+# allows use of more than 259 characters and its string representation is prepended with "\\?\". See
+# https://docs.python.org/3/library/os.html#os.readlink. This causes problems for us because the
+# behavior is not consistent (see https://github.com/certbot/certbot/pull/10136) and paths that have
+# this prefix won't match those that do not in simple equality comparisons.
 def readlink(*unused_args, **unused_kwargs):  # type: ignore
     """Method os.readlink() is forbidden"""
     raise RuntimeError('Usage of os.readlink() is forbidden. '
diff -Nru python-certbot-2.11.0/certbot/configuration.py python-certbot-4.0.0/certbot/configuration.py
--- python-certbot-2.11.0/certbot/configuration.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/configuration.py	2025-04-07 18:03:33.000000000 -0400
@@ -8,7 +8,6 @@
 from typing import List
 from typing import Optional
 from urllib import parse
-import warnings
 
 from certbot import errors
 from certbot import util
@@ -43,9 +42,7 @@
     paths defined in :py:mod:`certbot._internal.constants`:
 
       - `accounts_dir`
-      - `csr_dir`
       - `in_progress_dir`
-      - `key_dir`
       - `temp_checkpoint_dir`
 
     And the following paths are dynamically resolved using
@@ -286,25 +283,11 @@
         return os.path.join(self.namespace.work_dir, constants.BACKUP_DIR)
 
     @property
-    def csr_dir(self) -> str:
-        """Directory where new Certificate Signing Requests (CSRs) are saved."""
-        warnings.warn("NamespaceConfig.csr_dir is deprecated and will be removed in an upcoming "
-                      "release of Certbot", DeprecationWarning)
-        return os.path.join(self.namespace.config_dir, constants.CSR_DIR)
-
-    @property
     def in_progress_dir(self) -> str:
         """Directory used before a permanent checkpoint is finalized."""
         return os.path.join(self.namespace.work_dir, constants.IN_PROGRESS_DIR)
 
     @property
-    def key_dir(self) -> str:
-        """Keys storage."""
-        warnings.warn("NamespaceConfig.key_dir is deprecated and will be removed in an upcoming "
-                      "release of Certbot", DeprecationWarning)
-        return os.path.join(self.namespace.config_dir, constants.KEY_DIR)
-
-    @property
     def temp_checkpoint_dir(self) -> str:
         """Temporary checkpoint directory."""
         return os.path.join(
@@ -381,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 -Nru python-certbot-2.11.0/certbot/crypto_util.py python-certbot-4.0.0/certbot/crypto_util.py
--- python-certbot-2.11.0/certbot/crypto_util.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/crypto_util.py	2025-04-07 18:03:33.000000000 -0400
@@ -8,7 +8,6 @@
 import hashlib
 import logging
 import re
-from typing import Callable
 from typing import List
 from typing import Optional
 from typing import Set
@@ -21,7 +20,9 @@
 from cryptography.exceptions import UnsupportedAlgorithm
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives import serialization
 from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.asymmetric import rsa
 from cryptography.hazmat.primitives.asymmetric.dsa import DSAPublicKey
 from cryptography.hazmat.primitives.asymmetric.ec import ECDSA
 from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
@@ -30,10 +31,7 @@
 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
-import pyrfc3339
 
 from acme import crypto_util as acme_crypto_util
 from certbot import errors
@@ -142,7 +140,7 @@
 def valid_csr(csr: bytes) -> bool:
     """Validate CSR.
 
-    Check if `csr` is a valid CSR for the given domains.
+    Check if `csr` is a valid CSR with a correct self-signed signature.
 
     :param bytes csr: CSR in PEM.
 
@@ -151,10 +149,9 @@
 
     """
     try:
-        req = crypto.load_certificate_request(
-            crypto.FILETYPE_PEM, csr)
-        return req.verify(req.get_pubkey())
-    except crypto.Error:
+        req = x509.load_pem_x509_csr(csr)
+        return req.is_signature_valid
+    except (ValueError, TypeError):
         logger.debug("", exc_info=True)
         return False
 
@@ -169,43 +166,42 @@
     :rtype: bool
 
     """
-    req = crypto.load_certificate_request(
-        crypto.FILETYPE_PEM, csr)
-    pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, privkey)
-    try:
-        return req.verify(pkey)
-    except crypto.Error:
-        logger.debug("", exc_info=True)
-        return False
+    req = x509.load_pem_x509_csr(csr)
+    pkey = serialization.load_pem_private_key(privkey, password=None)
+    return req.is_signature_valid and req.public_key() == pkey.public_key()
 
 
-def import_csr_file(csrfile: str, data: bytes) -> Tuple[int, util.CSR, List[str]]:
+def import_csr_file(
+    csrfile: str, data: bytes
+) -> Tuple[acme_crypto_util.Format, util.CSR, List[str]]:
     """Import a CSR file, which can be either PEM or DER.
 
     :param str csrfile: CSR filename
     :param bytes data: contents of the CSR file
 
-    :returns: (`crypto.FILETYPE_PEM`,
+    :returns: (`acme_crypto_util.Format.PEM`,
                util.CSR object representing the CSR,
                list of domains requested in the CSR)
     :rtype: tuple
 
     """
-    PEM = crypto.FILETYPE_PEM
-    load = crypto.load_certificate_request
     try:
         # Try to parse as DER first, then fall back to PEM.
-        csr = load(crypto.FILETYPE_ASN1, data)
-    except crypto.Error:
+        csr = x509.load_der_x509_csr(data)
+    except ValueError:
         try:
-            csr = load(PEM, data)
-        except crypto.Error:
+            csr = x509.load_pem_x509_csr(data)
+        except ValueError:
             raise errors.Error("Failed to parse CSR file: {0}".format(csrfile))
 
-    domains = _get_names_from_loaded_cert_or_req(csr)
+    domains = acme_crypto_util.get_names_from_subject_and_extensions(csr.subject, csr.extensions)
     # Internally we always use PEM, so re-encode as PEM before returning.
-    data_pem = crypto.dump_certificate_request(PEM, csr)
-    return PEM, util.CSR(file=csrfile, data=data_pem, form="pem"), domains
+    data_pem = csr.public_bytes(serialization.Encoding.PEM)
+    return (
+        acme_crypto_util.Format.PEM,
+        util.CSR(file=csrfile, data=data_pem, form="pem"),
+        domains,
+    )
 
 
 def make_key(bits: int = 2048, key_type: str = "rsa",
@@ -218,14 +214,15 @@
 
     :returns: new RSA or ECDSA key in PEM form with specified number of bits
               or of type ec_curve when key_type ecdsa is used.
-    :rtype: str
+    :rtype: bytes
+
     """
+    key: Union[rsa.RSAPrivateKey, ec.EllipticCurvePrivateKey]
     if key_type == 'rsa':
         if bits < 2048:
             raise errors.Error("Unsupported RSA key length: {}".format(bits))
 
-        key = crypto.PKey()
-        key.generate_key(crypto.TYPE_RSA, bits)
+        key = rsa.generate_private_key(public_exponent=65537, key_size=bits)
     elif key_type == 'ecdsa':
         if not elliptic_curve:
             raise errors.Error("When key_type == ecdsa, elliptic_curve must be set.")
@@ -235,7 +232,7 @@
                 curve = getattr(ec, elliptic_curve.upper())
                 if not curve:
                     raise errors.Error(f"Invalid curve type: {elliptic_curve}")
-                _key = ec.generate_private_key(
+                key = ec.generate_private_key(
                     curve=curve(),
                     backend=default_backend()
                 )
@@ -245,15 +242,13 @@
             raise errors.Error("Unsupported elliptic curve: {}".format(elliptic_curve))
         except UnsupportedAlgorithm as e:
             raise e from errors.Error(str(e))
-        _key_pem = _key.private_bytes(
-            encoding=Encoding.PEM,
-            format=PrivateFormat.TraditionalOpenSSL,
-            encryption_algorithm=NoEncryption()
-        )
-        key = crypto.load_privatekey(crypto.FILETYPE_PEM, _key_pem)
     else:
         raise errors.Error("Invalid key_type specified: {}.  Use [rsa|ecdsa]".format(key_type))
-    return crypto.dump_privatekey(crypto.FILETYPE_PEM, key)
+    return key.private_bytes(
+        encoding=Encoding.PEM,
+        format=PrivateFormat.PKCS8,
+        encryption_algorithm=NoEncryption()
+    )
 
 
 def valid_privkey(privkey: Union[str, bytes]) -> bool:
@@ -265,11 +260,14 @@
     :rtype: bool
 
     """
+    if isinstance(privkey, str):
+        privkey = privkey.encode()
     try:
-        return crypto.load_privatekey(
-            crypto.FILETYPE_PEM, privkey).check()
-    except (TypeError, crypto.Error):
+        serialization.load_pem_private_key(privkey, password=None)
+    except ValueError:
         return False
+    else:
+        return True
 
 
 def verify_renewable_cert(renewable_cert: interfaces.RenewableCert) -> None:
@@ -307,7 +305,7 @@
         assert cert.signature_hash_algorithm # always present for RSA and ECDSA
         verify_signed_payload(pk, cert.signature, cert.tbs_certificate_bytes,
                                 cert.signature_hash_algorithm)
-    except (IOError, ValueError, InvalidSignature) as e:
+    except (OSError, ValueError, InvalidSignature) as e:
         error_str = "verifying the signature of the certificate located at {0} has failed. \
                 Details: {1}".format(renewable_cert.cert_path, e)
         logger.exception(error_str)
@@ -350,11 +348,11 @@
     :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()
-    except (IOError, SSL.Error) as e:
+    except (OSError, SSL.Error) as e:
         error_str = "verifying the certificate located at {0} matches the \
                 private key located at {1} has failed. \
                 Details: {2}".format(cert_path,
@@ -382,7 +380,7 @@
             error_str = "fullchain does not match cert + chain for {0}!"
             error_str = error_str.format(renewable_cert.lineagename)
             raise errors.Error(error_str)
-    except IOError as e:
+    except OSError as e:
         error_str = "reading one of cert, chain, or fullchain has failed: {0}".format(e)
         logger.exception(error_str)
         raise errors.Error(error_str)
@@ -390,109 +388,78 @@
         raise e
 
 
-def pyopenssl_load_certificate(data: bytes) -> Tuple[crypto.X509, int]:
-    """Load PEM/DER certificate.
-
-    :raises errors.Error:
-
-    """
-
-    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 _load_cert_or_req(cert_or_req_str: bytes,
-                      load_func: Callable[[int, bytes], Union[crypto.X509, crypto.X509Req]],
-                      typ: int = crypto.FILETYPE_PEM) -> Union[crypto.X509, crypto.X509Req]:
-    try:
-        return load_func(typ, cert_or_req_str)
-    except crypto.Error as err:
-        logger.debug("", exc_info=True)
-        logger.error("Encountered error while loading certificate or csr: %s", str(err))
-        raise
-
-
-def _get_sans_from_cert_or_req(cert_or_req_str: bytes,
-                               load_func: Callable[[int, bytes], Union[crypto.X509,
-                                                                       crypto.X509Req]],
-                               typ: int = crypto.FILETYPE_PEM) -> List[str]:
-    # pylint: disable=protected-access
-    return acme_crypto_util._pyopenssl_cert_or_req_san(_load_cert_or_req(
-        cert_or_req_str, load_func, typ))
-
-
-def get_sans_from_cert(cert: bytes, typ: int = crypto.FILETYPE_PEM) -> List[str]:
+def get_sans_from_cert(
+    cert: bytes, typ: Union[acme_crypto_util.Format, int] = acme_crypto_util.Format.PEM
+) -> List[str]:
     """Get a list of Subject Alternative Names from a certificate.
 
     :param str cert: Certificate (encoded).
-    :param typ: `crypto.FILETYPE_PEM` or `crypto.FILETYPE_ASN1`
+    :param Format typ: Which format the `cert` bytes are in.
 
     :returns: A list of Subject Alternative Names.
     :rtype: list
 
     """
-    return _get_sans_from_cert_or_req(
-        cert, crypto.load_certificate, typ)
-
-
-def _get_names_from_cert_or_req(cert_or_req: bytes,
-                                load_func: Callable[[int, bytes], Union[crypto.X509,
-                                                                        crypto.X509Req]],
-                                typ: int) -> List[str]:
-    loaded_cert_or_req = _load_cert_or_req(cert_or_req, load_func, typ)
-    return _get_names_from_loaded_cert_or_req(loaded_cert_or_req)
+    typ = acme_crypto_util.Format(typ)
+    if typ == acme_crypto_util.Format.PEM:
+        x509_cert = x509.load_pem_x509_certificate(cert)
+    else:
+        assert typ == acme_crypto_util.Format.DER
+        x509_cert = x509.load_der_x509_certificate(cert)
 
+    try:
+        san_ext = x509_cert.extensions.get_extension_for_class(
+            x509.SubjectAlternativeName
+        )
+    except x509.ExtensionNotFound:
+        return []
 
-def _get_names_from_loaded_cert_or_req(loaded_cert_or_req: Union[crypto.X509, crypto.X509Req]
-                                       ) -> List[str]:
-    # pylint: disable=protected-access
-    return acme_crypto_util._pyopenssl_cert_or_req_all_names(loaded_cert_or_req)
+    return san_ext.value.get_values_for_type(x509.DNSName)
 
 
-def get_names_from_cert(cert: bytes, typ: int = crypto.FILETYPE_PEM) -> List[str]:
+def get_names_from_cert(
+    cert: bytes, typ: Union[acme_crypto_util.Format, int] = acme_crypto_util.Format.PEM
+) -> List[str]:
     """Get a list of domains from a cert, including the CN if it is set.
 
     :param str cert: Certificate (encoded).
-    :param typ: `crypto.FILETYPE_PEM` or `crypto.FILETYPE_ASN1`
+    :param Format typ: Which format the `cert` bytes are in.
 
     :returns: A list of domain names.
     :rtype: list
 
     """
-    return _get_names_from_cert_or_req(
-        cert, crypto.load_certificate, typ)
+    typ = acme_crypto_util.Format(typ)
+    if typ == acme_crypto_util.Format.PEM:
+        x509_cert = x509.load_pem_x509_certificate(cert)
+    else:
+        assert typ == acme_crypto_util.Format.DER
+        x509_cert = x509.load_der_x509_certificate(cert)
+    return acme_crypto_util.get_names_from_subject_and_extensions(
+        x509_cert.subject, x509_cert.extensions
+    )
 
 
-def get_names_from_req(csr: bytes, typ: int = crypto.FILETYPE_PEM) -> List[str]:
+def get_names_from_req(
+    csr: bytes, typ: Union[acme_crypto_util.Format, int] = acme_crypto_util.Format.PEM
+) -> List[str]:
     """Get a list of domains from a CSR, including the CN if it is set.
 
     :param str csr: CSR (encoded).
-    :param typ: `crypto.FILETYPE_PEM` or `crypto.FILETYPE_ASN1`
+    :param acme_crypto_util.Format typ: Which format the `csr` bytes are in.
     :returns: A list of domain names.
     :rtype: list
 
     """
-    return _get_names_from_cert_or_req(csr, crypto.load_certificate_request, typ)
-
-
-def dump_pyopenssl_chain(chain: Union[List[crypto.X509], List[josepy.ComparableX509]],
-                         filetype: int = crypto.FILETYPE_PEM) -> bytes:
-    """Dump certificate chain into a bundle.
-
-    :param list chain: List of `crypto.X509` (or wrapped in
-        :class:`josepy.util.ComparableX509`).
-
-    """
-    # 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)
+    typ = acme_crypto_util.Format(typ)
+    if typ == acme_crypto_util.Format.PEM:
+        x509_req = x509.load_pem_x509_csr(csr)
+    else:
+        assert typ == acme_crypto_util.Format.DER
+        x509_req = x509.load_der_x509_csr(csr)
+    return acme_crypto_util.get_names_from_subject_and_extensions(
+        x509_req.subject, x509_req.extensions
+    )
 
 
 def notBefore(cert_path: str) -> datetime.datetime:
@@ -504,7 +471,9 @@
     :rtype: :class:`datetime.datetime`
 
     """
-    return _notAfterBefore(cert_path, crypto.X509.get_notBefore)
+    with open(cert_path, "rb") as f:
+        cert = x509.load_pem_x509_certificate(f.read())
+    return cert.not_valid_before_utc
 
 
 def notAfter(cert_path: str) -> datetime.datetime:
@@ -516,35 +485,9 @@
     :rtype: :class:`datetime.datetime`
 
     """
-    return _notAfterBefore(cert_path, crypto.X509.get_notAfter)
-
-
-def _notAfterBefore(cert_path: str,
-                    method: Callable[[crypto.X509], Optional[bytes]]) -> datetime.datetime:
-    """Internal helper function for finding notbefore/notafter.
-
-    :param str cert_path: path to a cert in PEM format
-    :param function method: one of ``crypto.X509.get_notBefore``
-        or ``crypto.X509.get_notAfter``
-
-    :returns: the notBefore or notAfter value from the cert at cert_path
-    :rtype: :class:`datetime.datetime`
-
-    """
-    # pylint: disable=redefined-outer-name
     with open(cert_path, "rb") as f:
-        x509 = crypto.load_certificate(crypto.FILETYPE_PEM, f.read())
-    # pyopenssl always returns bytes
-    timestamp = method(x509)
-    if not timestamp:
-        raise errors.Error("Error while invoking timestamp method, None has been returned.")
-    reformatted_timestamp = [timestamp[0:4], b"-", timestamp[4:6], b"-",
-                             timestamp[6:8], b"T", timestamp[8:10], b":",
-                             timestamp[10:12], b":", timestamp[12:]]
-    # pyrfc3339 always uses the type `str`
-    timestamp_bytes = b"".join(reformatted_timestamp)
-    timestamp_str = timestamp_bytes.decode('ascii')
-    return pyrfc3339.parse(timestamp_str)
+        cert = x509.load_pem_x509_certificate(f.read())
+    return cert.not_valid_after_utc
 
 
 def sha256sum(filename: str) -> str:
@@ -593,10 +536,13 @@
         raise errors.Error("failed to parse fullchain into cert and chain: " +
                            "less than 2 certificates in chain")
 
-    # Second pass: for each certificate found, parse it using OpenSSL and re-encode it,
+    # Second pass: for each certificate found, parse it using cryptography and re-encode it,
     # with the effect of normalizing any encoding variations (e.g. CRLF, whitespace).
-    certs_normalized = [crypto.dump_certificate(crypto.FILETYPE_PEM,
-        crypto.load_certificate(crypto.FILETYPE_PEM, cert)).decode() for cert in certs]
+    certs_normalized: List[str] = []
+    for cert_pem in certs:
+        cert = x509.load_pem_x509_certificate(cert_pem)
+        cert_pem = cert.public_bytes(Encoding.PEM)
+        certs_normalized.append(cert_pem.decode())
 
     # Since each normalized cert has a newline suffix, no extra newlines are required.
     return (certs_normalized[0], "".join(certs_normalized[1:]))
@@ -610,10 +556,9 @@
     :returns: serial number of the certificate
     :rtype: int
     """
-    # pylint: disable=redefined-outer-name
     with open(cert_path, "rb") as f:
-        x509 = crypto.load_certificate(crypto.FILETYPE_PEM, f.read())
-    return x509.get_serial_number()
+        cert = x509.load_pem_x509_certificate(f.read())
+    return cert.serial_number
 
 
 def find_chain_with_issuer(fullchains: List[str], issuer_cn: str,
diff -Nru python-certbot-2.11.0/certbot/display/ops.py python-certbot-4.0.0/certbot/display/ops.py
--- python-certbot-2.11.0/certbot/display/ops.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/display/ops.py	2025-04-07 18:03:33.000000000 -0400
@@ -19,12 +19,10 @@
 logger = logging.getLogger(__name__)
 
 
-def get_email(invalid: bool = False, optional: bool = True) -> str:
+def get_email(invalid: bool = False, **kwargs: Any) -> str:
     """Prompt for valid email address.
 
     :param bool invalid: True if an invalid address was provided by the user
-    :param bool optional: True if the user can use
-        --register-unsafely-without-email to avoid providing an e-mail
 
     :returns: e-mail address
     :rtype: str
@@ -32,45 +30,22 @@
     :raises errors.Error: if the user cancels
 
     """
-    invalid_prefix = "There seem to be problems with that address. "
-    msg = "Enter email address (used for urgent renewal and security notices)\n"
-    unsafe_suggestion = ("\n\nIf you really want to skip this, you can run "
-                         "the client with --register-unsafely-without-email "
-                         "but you will then 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.\n\n")
-    if optional:
-        if invalid:
-            msg += unsafe_suggestion
-            suggest_unsafe = False
-        else:
-            suggest_unsafe = True
-    else:
-        suggest_unsafe = False
+    # pylint: disable=unused-argument
+    invalid_prefix = ""
+    if invalid:
+        invalid_prefix = "The server reported a problem with your email address. "
+    msg = "Enter email address or hit Enter to skip.\n"
 
     while True:
-        try:
-            code, email = display_util.input_text(invalid_prefix + msg if invalid else msg,
-                                                  force_interactive=True)
-        except errors.MissingCommandlineFlag:
-            msg = ("You should register before running non-interactively, "
-                   "or provide --agree-tos and --email <email_address> flags.")
-            raise errors.MissingCommandlineFlag(msg)
+        code, email = display_util.input_text(invalid_prefix + msg, default="")
 
         if code != display_util.OK:
-            if optional:
-                raise errors.Error(
-                    "An e-mail address or "
-                    "--register-unsafely-without-email must be provided.")
-            raise errors.Error("An e-mail address must be provided.")
+            raise errors.Error("Error getting email address.")
+        if email == "":
+            return ""
         if util.safe_email(email):
             return email
-        if suggest_unsafe:
-            msg = unsafe_suggestion + msg
-            suggest_unsafe = False  # add this message at most once
-
-        invalid = bool(email)
+        invalid_prefix = "There is a problem with your email address. "
 
 
 def choose_account(accounts: List[account.Account]) -> Optional[account.Account]:
diff -Nru python-certbot-2.11.0/certbot/__init__.py python-certbot-4.0.0/certbot/__init__.py
--- python-certbot-2.11.0/certbot/__init__.py	2024-06-05 17:34:03.000000000 -0400
+++ python-certbot-4.0.0/certbot/__init__.py	2025-04-07 18:03:33.000000000 -0400
@@ -1,4 +1,4 @@
 """Certbot client."""
 
 # version number like 1.2.3a0, must have at least 2 parts, like 1.2
-__version__ = '2.11.0'
+__version__ = '4.0.0'
diff -Nru python-certbot-2.11.0/certbot/_internal/account.py python-certbot-4.0.0/certbot/_internal/account.py
--- python-certbot-2.11.0/certbot/_internal/account.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/account.py	2025-04-07 18:03:33.000000000 -0400
@@ -223,7 +223,7 @@
                 key = jose.JWK.json_loads(key_file.read())
             with open(self._metadata_path(account_dir_path)) as metadata_file:
                 meta = Account.Meta.json_loads(metadata_file.read())
-        except IOError as error:
+        except OSError as error:
             raise errors.AccountStorageError(error)
 
         return Account(regr, key, meta)
@@ -243,7 +243,7 @@
             self._create(account, dir_path)
             self._update_meta(account, dir_path)
             self._update_regr(account, dir_path)
-        except IOError as error:
+        except OSError as error:
             raise errors.AccountStorageError(error)
 
     def update_regr(self, account: Account) -> None:
@@ -255,7 +255,7 @@
         try:
             dir_path = self._prepare(account)
             self._update_regr(account, dir_path)
-        except IOError as error:
+        except OSError as error:
             raise errors.AccountStorageError(error)
 
     def update_meta(self, account: Account) -> None:
@@ -267,7 +267,7 @@
         try:
             dir_path = self._prepare(account)
             self._update_meta(account, dir_path)
-        except IOError as error:
+        except OSError as error:
             raise errors.AccountStorageError(error)
 
     def delete(self, account_id: str) -> None:
diff -Nru python-certbot-2.11.0/certbot/_internal/cert_manager.py python-certbot-4.0.0/certbot/_internal/cert_manager.py
--- python-certbot-2.11.0/certbot/_internal/cert_manager.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/cert_manager.py	2025-04-07 18:03:33.000000000 -0400
@@ -30,22 +30,6 @@
 ###################
 
 
-def update_live_symlinks(config: configuration.NamespaceConfig) -> None:
-    """Update the certificate file family symlinks to use archive_dir.
-
-    Use the information in the config file to make symlinks point to
-    the correct archive directory.
-
-    .. note:: This assumes that the installation is using a Reverter object.
-
-    :param config: Configuration.
-    :type config: :class:`certbot._internal.configuration.NamespaceConfig`
-
-    """
-    for renewal_file in storage.renewal_conf_files(config):
-        storage.RenewableCert(renewal_file, config, update_symlinks=True)
-
-
 def rename_lineage(config: configuration.NamespaceConfig) -> None:
     """Rename the specified lineage to the new name.
 
@@ -133,7 +117,7 @@
         return None
     try:
         return storage.RenewableCert(renewal_file, cli_config)
-    except (errors.CertStorageError, IOError):
+    except (OSError, errors.CertStorageError):
         logger.debug("Renewal conf file %s is broken.", renewal_file)
         logger.debug("Traceback was:\n%s", traceback.format_exc())
         return None
@@ -435,7 +419,7 @@
     for renewal_file in storage.renewal_conf_files(cli_config):
         try:
             candidate_lineage = storage.RenewableCert(renewal_file, cli_config)
-        except (errors.CertStorageError, IOError):
+        except (OSError, errors.CertStorageError):
             logger.debug("Renewal conf file %s is broken. Skipping.", renewal_file)
             logger.debug("Traceback was:\n%s", traceback.format_exc())
             continue
diff -Nru python-certbot-2.11.0/certbot/_internal/cli/cli_utils.py python-certbot-4.0.0/certbot/_internal/cli/cli_utils.py
--- python-certbot-2.11.0/certbot/_internal/cli/cli_utils.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/cli/cli_utils.py	2025-04-07 18:03:33.000000000 -0400
@@ -40,7 +40,7 @@
         with open(filename, mode) as the_file:
             contents = the_file.read()
         return filename, contents
-    except IOError as exc:
+    except OSError as exc:
         raise argparse.ArgumentTypeError(exc.strerror)
 
 
diff -Nru python-certbot-2.11.0/certbot/_internal/cli/helpful.py python-certbot-4.0.0/certbot/_internal/cli/helpful.py
--- python-certbot-2.11.0/certbot/_internal/cli/helpful.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/cli/helpful.py	2025-04-07 18:03:33.000000000 -0400
@@ -58,7 +58,6 @@
             "revoke": main.revoke,
             "rollback": main.rollback,
             "everything": main.run,
-            "update_symlinks": main.update_symlinks,
             "certificates": main.certificates,
             "delete": main.delete,
             "enhance": main.enhance,
diff -Nru python-certbot-2.11.0/certbot/_internal/cli/__init__.py python-certbot-4.0.0/certbot/_internal/cli/__init__.py
--- python-certbot-2.11.0/certbot/_internal/cli/__init__.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/cli/__init__.py	2025-04-07 18:03:33.000000000 -0400
@@ -171,21 +171,17 @@
     helpful.add(
         ["register", "automation"], "--register-unsafely-without-email", action="store_true",
         default=flag_default("register_unsafely_without_email"),
-        help="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.")
+        help=argparse.SUPPRESS)
     helpful.add(
         ["register", "update_account", "unregister", "automation"], "-m", "--email",
         default=flag_default("email"),
         help=config_help("email"))
     helpful.add(["register", "update_account", "automation"], "--eff-email", action="store_true",
                 default=flag_default("eff_email"), dest="eff_email",
-                help="Share your e-mail address with EFF")
+                help="Share your e-mail address with EFF (default: Ask)")
     helpful.add(["register", "update_account", "automation"], "--no-eff-email",
                 action="store_false", default=flag_default("eff_email"), dest="eff_email",
-                help="Don't share your e-mail address with EFF")
+                help="Don't share your e-mail address with EFF (default: Ask)")
     helpful.add(
         ["automation", "certonly", "run"],
         "--keep-until-expiring", "--keep", "--reinstall",
@@ -336,7 +332,7 @@
         "--hsts", action="store_true", dest="hsts", default=flag_default("hsts"),
         help="Add the Strict-Transport-Security header to every HTTP response."
              " Forcing browser to always use SSL for the domain."
-             " Defends against SSL Stripping.")
+             " Defends against SSL Stripping. (default: False)")
     helpful.add(
         "security", "--no-hsts", action="store_false", dest="hsts",
         default=flag_default("hsts"), help=argparse.SUPPRESS)
@@ -345,7 +341,7 @@
         "--uir", action="store_true", dest="uir", default=flag_default("uir"),
         help='Add the "Content-Security-Policy: upgrade-insecure-requests"'
              ' header to every HTTP response. Forcing the browser to use'
-             ' https:// for every http:// resource.')
+             ' https:// for every http:// resource. (default: False)')
     helpful.add(
         "security", "--no-uir", action="store_false", dest="uir", default=flag_default("uir"),
         help=argparse.SUPPRESS)
@@ -353,7 +349,7 @@
         "security", "--staple-ocsp", action="store_true", dest="staple",
         default=flag_default("staple"),
         help="Enables OCSP Stapling. A valid OCSP response is stapled to"
-        " the certificate that the server offers during TLS.")
+        " the certificate that the server offers during TLS. (default: False)")
     helpful.add(
         "security", "--no-staple-ocsp", action="store_false", dest="staple",
         default=flag_default("staple"), help=argparse.SUPPRESS)
@@ -364,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")
     )
@@ -442,8 +448,8 @@
     helpful.add(
         "renew", "--no-directory-hooks", action="store_false",
         default=flag_default("directory_hooks"), dest="directory_hooks",
-        help="Disable running executables found in Certbot's hook directories"
-        " during renewal. (default: False)")
+        help="Disable running executables found in Certbot's hook directories."
+        " (default: False)")
     helpful.add(
         "renew", "--disable-renew-updates", action="store_true",
         default=flag_default("disable_renew_updates"), dest="disable_renew_updates",
diff -Nru python-certbot-2.11.0/certbot/_internal/cli/subparsers.py python-certbot-4.0.0/certbot/_internal/cli/subparsers.py
--- python-certbot-2.11.0/certbot/_internal/cli/subparsers.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/cli/subparsers.py	2025-04-07 18:03:33.000000000 -0400
@@ -44,7 +44,7 @@
                 "--delete-after-revoke", action="store_true",
                 default=flag_default("delete_after_revoke"),
                 help="Delete certificates after revoking them, along with all previous and later "
-                "versions of those certificates.")
+                "versions of those certificates. (default: Ask)")
     helpful.add("revoke",
                 "--no-delete-after-revoke", action="store_false",
                 dest="delete_after_revoke",
@@ -52,7 +52,7 @@
                 help="Do not delete certificates after revoking them. This "
                      "option should be used with caution because the 'renew' "
                      "subcommand will attempt to renew undeleted revoked "
-                     "certificates.")
+                     "certificates. (default: Ask)")
     helpful.add("rollback",
                 "--checkpoints", type=int, metavar="N",
                 default=flag_default("rollback_checkpoints"),
diff -Nru python-certbot-2.11.0/certbot/_internal/client.py python-certbot-4.0.0/certbot/_internal/client.py
--- python-certbot-2.11.0/certbot/_internal/client.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/client.py	2025-04-07 18:03:33.000000000 -0400
@@ -11,14 +11,15 @@
 from typing import Optional
 from typing import Tuple
 
+from cryptography import x509
 from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
 from cryptography.hazmat.primitives.asymmetric.rsa import generate_private_key
 import josepy as jose
 from josepy import ES256
 from josepy import ES384
 from josepy import ES512
 from josepy import RS256
-import OpenSSL
 
 from acme import client as acme_client
 from acme import crypto_util as acme_crypto_util
@@ -47,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':
@@ -185,15 +187,9 @@
     # Log non-standard actions, potentially wrong API calls
     if account_storage.find_all():
         logger.info("There are already existing accounts for %s", config.server)
-    if config.email is None:
-        if not config.register_unsafely_without_email:
-            msg = ("No email was provided and "
-                   "--register-unsafely-without-email was not present.")
-            logger.error(msg)
-            raise errors.Error(msg)
-        if not config.dry_run:
-            logger.debug("Registering without email!")
 
+    if config.email == "":
+        config.email = None
     # If --dry-run is used, and there is no staging account, create one with no email.
     if config.dry_run:
         config.email = None
@@ -433,7 +429,7 @@
             if self.config.allow_subset_of_names:
                 successful_domains = self._successful_domains_from_error(error, domains)
                 if successful_domains != domains and len(successful_domains) != 0:
-                    return self._retry_obtain_certificate(domains, successful_domains)
+                    return self._retry_obtain_certificate(domains, successful_domains, old_keypath)
             raise
         authzr = orderr.authorizations
         auth_domains = {a.body.identifier.value for a in authzr}
@@ -445,7 +441,7 @@
         # domains contains a wildcard because the ACME spec forbids identifiers
         # in authzs from containing a wildcard character.
         if self.config.allow_subset_of_names and successful_domains != domains:
-            return self._retry_obtain_certificate(domains, successful_domains)
+            return self._retry_obtain_certificate(domains, successful_domains, old_keypath)
         else:
             try:
                 cert, chain = self.obtain_certificate_from_csr(csr, orderr)
@@ -457,7 +453,8 @@
                 if self.config.allow_subset_of_names:
                     successful_domains = self._successful_domains_from_error(error, domains)
                     if successful_domains != domains and len(successful_domains) != 0:
-                        return self._retry_obtain_certificate(domains, successful_domains)
+                        return self._retry_obtain_certificate(
+                            domains, successful_domains, old_keypath)
                 raise
 
     def _get_order_and_authorizations(self, csr_pem: bytes,
@@ -474,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.")
@@ -488,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.")
@@ -539,13 +545,14 @@
             return successful_domains
         return []
 
-    def _retry_obtain_certificate(self, domains: List[str], successful_domains: List[str]
+    def _retry_obtain_certificate(self, domains: List[str], successful_domains: List[str],
+                                old_keypath: Optional[str]
                                 ) -> Tuple[bytes, bytes, util.Key, util.CSR]:
         failed_domains = [d for d in domains if d not in successful_domains]
         domains_list = ", ".join(failed_domains)
         display_util.notify("Unable to obtain a certificate with every requested "
             f"domain. Retrying without: {domains_list}")
-        return self.obtain_certificate(successful_domains)
+        return self.obtain_certificate(successful_domains, old_keypath)
 
     def _choose_lineagename(self, domains: List[str], certname: Optional[str]) -> str:
         """Chooses a name for the new lineage.
@@ -804,12 +811,9 @@
 
     if csr:
         if csr.form == "der":
-            csr_obj = OpenSSL.crypto.load_certificate_request(
-                OpenSSL.crypto.FILETYPE_ASN1, csr.data)
-            cert_buffer = OpenSSL.crypto.dump_certificate_request(
-                OpenSSL.crypto.FILETYPE_PEM, csr_obj
-            )
-            csr = util.CSR(csr.file, cert_buffer, "pem")
+            csr_obj = x509.load_der_x509_csr(csr.data)
+            csr_pem = csr_obj.public_bytes(serialization.Encoding.PEM)
+            csr = util.CSR(csr.file, csr_pem, "pem")
 
         # If CSR is provided, it must be readable and valid.
         if csr.data and not crypto_util.valid_csr(csr.data):
diff -Nru python-certbot-2.11.0/certbot/_internal/constants.py python-certbot-4.0.0/certbot/_internal/constants.py
--- python-certbot-2.11.0/certbot/_internal/constants.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/constants.py	2025-04-07 18:03:33.000000000 -0400
@@ -1,7 +1,7 @@
 """Certbot constants."""
 import atexit
+import importlib.resources
 import logging
-import sys
 from contextlib import ExitStack
 from typing import Any
 from typing import Dict
@@ -10,11 +10,6 @@
 from certbot.compat import misc
 from certbot.compat import os
 
-if sys.version_info >= (3, 9):  # pragma: no cover
-    import importlib.resources as importlib_resources
-else:  # pragma: no cover
-    import importlib_resources
-
 SETUPTOOLS_PLUGINS_ENTRY_POINT = "certbot.plugins"
 """Setuptools entry point group name for plugins."""
 
@@ -42,7 +37,7 @@
     dry_run=False,
     register_unsafely_without_email=False,
     email=None,
-    eff_email=None,
+    eff_email=None, # listed as Ask in help output
     reinstall=False,
     expand=False,
     renew_by_default=False,
@@ -69,12 +64,14 @@
     elliptic_curve="secp256r1",
     key_type="ecdsa",
     must_staple=False,
-    redirect=None,
+    redirect=None, # default described manually in text in help output
     auto_hsts=False,
-    hsts=None,
-    uir=None,
-    staple=None,
+    hsts=None, # listed as False in help output
+    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,
@@ -94,7 +91,7 @@
     user_agent_comment=None,
     csr=None,
     reason=0,
-    delete_after_revoke=None,
+    delete_after_revoke=None, # listed as Ask in help output
     rollback_checkpoints=1,
     init=False,
     prepare=False,
@@ -182,9 +179,6 @@
 """Directory (relative to `certbot.configuration.NamespaceConfig.work_dir`)
 where backups are kept."""
 
-CSR_DIR = "csr"
-"""See `certbot.configuration.NamespaceConfig.csr_dir`."""
-
 IN_PROGRESS_DIR = "IN_PROGRESS"
 """Directory used before a permanent checkpoint is finalized (relative to
 `certbot.configuration.NamespaceConfig.work_dir`)."""
@@ -231,8 +225,8 @@
     # Python process, and will be automatically cleaned up on exit.
     file_manager = ExitStack()
     atexit.register(file_manager.close)
-    ssl_dhparams_src_ref = importlib_resources.files("certbot") / "ssl-dhparams.pem"
-    return str(file_manager.enter_context(importlib_resources.as_file(ssl_dhparams_src_ref)))
+    ssl_dhparams_src_ref = importlib.resources.files("certbot") / "ssl-dhparams.pem"
+    return str(file_manager.enter_context(importlib.resources.as_file(ssl_dhparams_src_ref)))
 
 SSL_DHPARAMS_SRC = _generate_ssl_dhparams_src_static()
 """Path to the nginx ssl_dhparams file found in the Certbot distribution."""
diff -Nru python-certbot-2.11.0/certbot/_internal/hooks.py python-certbot-4.0.0/certbot/_internal/hooks.py
--- python-certbot-2.11.0/certbot/_internal/hooks.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/hooks.py	2025-04-07 18:03:33.000000000 -0400
@@ -75,13 +75,11 @@
     :param configuration.NamespaceConfig config: Certbot settings
 
     """
-    if config.verb == "renew" and config.directory_hooks:
-        for hook in list_hooks(config.renewal_pre_hooks_dir):
-            _run_pre_hook_if_necessary(hook)
-
-    cmd = config.pre_hook
-    if cmd:
-        _run_pre_hook_if_necessary(cmd)
+    all_hooks: List[str] = (list_hooks(config.renewal_pre_hooks_dir) if config.directory_hooks
+        else [])
+    all_hooks += [config.pre_hook] if config.pre_hook else []
+    for hook in all_hooks:
+        _run_pre_hook_if_necessary(hook)
 
 
 executed_pre_hooks: Set[str] = set()
@@ -125,32 +123,31 @@
 
     """
 
-    cmd = config.post_hook
+    all_hooks: List[str] = (list_hooks(config.renewal_post_hooks_dir) if config.directory_hooks
+        else [])
+    all_hooks += [config.post_hook] if config.post_hook else []
     # In the "renew" case, we save these up to run at the end
     if config.verb == "renew":
-        if config.directory_hooks:
-            for hook in list_hooks(config.renewal_post_hooks_dir):
-                _run_eventually(hook)
-        if cmd:
-            _run_eventually(cmd)
+        for hook in all_hooks:
+            _run_eventually(hook)
     # certonly / run
-    elif cmd:
+    else:
         renewed_domains_str = ' '.join(renewed_domains)
         # 32k is reasonable on Windows and likely quite conservative on other platforms
         if len(renewed_domains_str) > 32_000:
             logger.warning("Limiting RENEWED_DOMAINS environment variable to 32k characters")
             renewed_domains_str = renewed_domains_str[:32_000]
-
-        _run_hook(
-            "post-hook",
-            cmd,
-            {
-                'RENEWED_DOMAINS': renewed_domains_str,
-                # Since other commands stop certbot execution on failure,
-                # it doesn't make sense to have a FAILED_DOMAINS variable
-                'FAILED_DOMAINS': ""
-            }
-        )
+        for hook in all_hooks:
+            _run_hook(
+                "post-hook",
+                hook,
+                {
+                    'RENEWED_DOMAINS': renewed_domains_str,
+                    # Since other commands stop certbot execution on failure,
+                    # it doesn't make sense to have a FAILED_DOMAINS variable
+                    'FAILED_DOMAINS': ""
+                }
+            )
 
 
 post_hooks: List[str] = []
@@ -228,19 +225,16 @@
     :param str lineage_path: live directory path for the new cert
 
     """
-    executed_dir_hooks = set()
-    if config.directory_hooks:
-        for hook in list_hooks(config.renewal_deploy_hooks_dir):
-            _run_deploy_hook(hook, domains, lineage_path, config.dry_run, config.run_deploy_hooks)
-            executed_dir_hooks.add(hook)
-
-    if config.renew_hook:
-        if config.renew_hook in executed_dir_hooks:
-            logger.info("Skipping deploy-hook '%s' as it was already run.",
-                        config.renew_hook)
+    executed_hooks = set()
+    all_hooks: List[str] = (list_hooks(config.renewal_deploy_hooks_dir)if config.directory_hooks
+        else [])
+    all_hooks += [config.renew_hook] if config.renew_hook else []
+    for hook in all_hooks:
+        if hook in executed_hooks:
+            logger.info("Skipping deploy-hook '%s' as it was already run.", hook)
         else:
-            _run_deploy_hook(config.renew_hook, domains,
-                             lineage_path, config.dry_run, config.run_deploy_hooks)
+            _run_deploy_hook(hook, domains, lineage_path, config.dry_run, config.run_deploy_hooks)
+            executed_hooks.add(hook)
 
 
 def _run_deploy_hook(command: str, domains: List[str], lineage_path: str, dry_run: bool,
diff -Nru python-certbot-2.11.0/certbot/_internal/lock.py python-certbot-4.0.0/certbot/_internal/lock.py
--- python-certbot-2.11.0/certbot/_internal/lock.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/lock.py	2025-04-07 18:03:33.000000000 -0400
@@ -124,7 +124,7 @@
         """
         try:
             fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
-        except IOError as err:
+        except OSError as err:
             if err.errno in (errno.EACCES, errno.EAGAIN):
                 logger.debug('A lock on %s is held by another process.', self._path)
                 raise errors.LockError('Another instance of Certbot is already running.')
@@ -187,13 +187,13 @@
     By default on Windows, acquiring a file handler gives exclusive access to the process
     and results in an effective lock. However, it is possible to explicitly acquire the
     file handler in shared access in terms of read and write, and this is done by os.open
-    and io.open in Python. So an explicit lock needs to be done through the call of
+    in Python. So an explicit lock needs to be done through the call of
     msvcrt.locking, that will lock the first byte of the file. In theory, it is also
     possible to access a file in shared delete access, allowing other processes to delete an
     opened file. But this needs also to be done explicitly by all processes using the Windows
     low level APIs, and Python does not do it. As of Python 3.7 and below, Python developers
     state that deleting a file opened by a process from another process is not possible with
-    os.open and io.open.
+    os.open.
     Consequently, msvcrt.locking is sufficient to obtain an effective lock, and the race
     condition encountered on Linux is not possible on Windows, leading to a simpler workflow.
     """
@@ -210,7 +210,7 @@
             # are only defined on Windows. See
             # https://github.com/python/typeshed/blob/16ae4c61201cd8b96b8b22cdfb2ab9e89ba5bcf2/stdlib/msvcrt.pyi.
             msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)  # type: ignore # pylint: disable=used-before-assignment
-        except (IOError, OSError) as err:
+        except OSError as err:
             if fd:
                 os.close(fd)
             # Anything except EACCES is unexpected. Raise directly the error in that case.
diff -Nru python-certbot-2.11.0/certbot/_internal/log.py python-certbot-4.0.0/certbot/_internal/log.py
--- python-certbot-2.11.0/certbot/_internal/log.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/log.py	2025-04-07 18:03:33.000000000 -0400
@@ -167,7 +167,7 @@
         handler = logging.handlers.RotatingFileHandler(
             log_file_path, maxBytes=2 ** 20,
             backupCount=config.max_log_backups)
-    except IOError as error:
+    except OSError as error:
         raise errors.Error(util.PERM_ERR_FMT.format(error))
     # rotate on each invocation, rollover only possible when maxBytes
     # is nonzero and backupCount is nonzero, so we set maxBytes as big
diff -Nru python-certbot-2.11.0/certbot/_internal/main.py python-certbot-4.0.0/certbot/_internal/main.py
--- python-certbot-2.11.0/certbot/_internal/main.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/main.py	2025-04-07 18:03:33.000000000 -0400
@@ -15,9 +15,9 @@
 from typing import Tuple
 from typing import TypeVar
 from typing import Union
-import warnings
 
 import configobj
+from cryptography import x509
 import josepy as jose
 from josepy import b64
 
@@ -141,8 +141,6 @@
                 )
             )
             lineage = le_client.obtain_and_enroll_certificate(domains, certname)
-            if lineage is False:
-                raise errors.Error("Certificate could not be obtained")
             if lineage is not None:
                 hooks.deploy_hook(config, lineage.names(), lineage.live_dir)
                 renewed_domains.extend(domains)
@@ -281,7 +279,8 @@
 
     if config.verb == "run":
         keep_opt = "Attempt to reinstall this existing certificate"
-    elif config.verb == "certonly":
+    else:
+        assert config.verb == "certonly", "Unexpected Certbot subcommand"
         keep_opt = "Keep the existing certificate for now"
     choices = [keep_opt,
                "Renew & replace the certificate (may be subject to CA rate limits)"]
@@ -711,8 +710,8 @@
     def _tos_cb(terms_of_service: str) -> None:
         if config.tos:
             return
-        msg = ("Please read the Terms of Service at {0}. You "
-               "must agree in order to register with the ACME "
+        msg = ("Please read the Terms of Service at: {0}\n"
+               "You must agree in order to register with the ACME "
                "server. Do you agree?".format(terms_of_service))
         result = display_util.yesno(msg, cli_flag="--agree-tos", force_interactive=True)
         if not result:
@@ -934,7 +933,7 @@
     if not accounts:
         return f"Could not find an existing account for server {config.server}."
     if config.email is None and not config.register_unsafely_without_email:
-        config.email = display_ops.get_email(optional=False)
+        config.email = display_ops.get_email()
 
     acc, acme = _determine_account(config)
     cb_client = client.Client(config, acc, None, None, acme=acme)
@@ -1266,27 +1265,6 @@
     client.rollback(config.installer, config.checkpoints, config, plugins)
 
 
-def update_symlinks(config: configuration.NamespaceConfig,
-                    unused_plugins: plugins_disco.PluginsRegistry) -> None:
-    """Update the certificate file family symlinks
-
-    Use the information in the config file to make symlinks point to
-    the correct archive directory.
-
-    :param config: Configuration object
-    :type config: configuration.NamespaceConfig
-
-    :param unused_plugins: List of plugins (deprecated)
-    :type unused_plugins: plugins_disco.PluginsRegistry
-
-    :returns: `None`
-    :rtype: None
-
-    """
-    warnings.warn("update_symlinks is deprecated and will be removed", PendingDeprecationWarning)
-    cert_manager.update_live_symlinks(config)
-
-
 def rename(config: configuration.NamespaceConfig,
            unused_plugins: plugins_disco.PluginsRegistry) -> None:
     """Rename a certificate
@@ -1387,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 -Nru python-certbot-2.11.0/certbot/_internal/plugins/disco.py python-certbot-4.0.0/certbot/_internal/plugins/disco.py
--- python-certbot-2.11.0/certbot/_internal/plugins/disco.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/plugins/disco.py	2025-04-07 18:03:33.000000000 -0400
@@ -176,7 +176,12 @@
 
     @classmethod
     def find_all(cls) -> 'PluginsRegistry':
-        """Find plugins using setuptools entry points."""
+        """Find plugins using Python package entry points.
+
+        See https://packaging.python.org/en/latest/specifications/entry-points/ for more info on
+        entry points.
+
+        """
         plugins: Dict[str, PluginEntryPoint] = {}
         plugin_paths_string = os.getenv('CERTBOT_PLUGIN_PATH')
         plugin_paths = plugin_paths_string.split(':') if plugin_paths_string else []
diff -Nru python-certbot-2.11.0/certbot/_internal/plugins/manual.py python-certbot-4.0.0/certbot/_internal/plugins/manual.py
--- python-certbot-2.11.0/certbot/_internal/plugins/manual.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/plugins/manual.py	2025-04-07 18:03:33.000000000 -0400
@@ -108,7 +108,6 @@
             help='Path or command to execute for the authentication script')
         add('cleanup-hook',
             help='Path or command to execute for the cleanup script')
-        util.add_deprecated_argument(add, 'public-ip-logging-ok', 0)
 
     def prepare(self) -> None:  # pylint: disable=missing-function-docstring
         if self.config.noninteractive_mode and not self.conf('auth-hook'):
diff -Nru python-certbot-2.11.0/certbot/_internal/plugins/standalone.py python-certbot-4.0.0/certbot/_internal/plugins/standalone.py
--- python-certbot-2.11.0/certbot/_internal/plugins/standalone.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/plugins/standalone.py	2025-04-07 18:03:33.000000000 -0400
@@ -2,7 +2,6 @@
 import collections
 import errno
 import logging
-import socket
 from typing import Any
 from typing import Callable
 from typing import DefaultDict
@@ -13,10 +12,9 @@
 from typing import Set
 from typing import Tuple
 from typing import Type
+from typing import Union
 from typing import TYPE_CHECKING
 
-from OpenSSL import crypto
-
 from acme import challenges
 from acme import standalone as acme_standalone
 from certbot import achallenges
@@ -25,6 +23,10 @@
 from certbot.display import util as display_util
 from certbot.plugins import common
 
+from cryptography import x509
+from cryptography.hazmat.primitives.asymmetric import types
+from OpenSSL import crypto
+
 logger = logging.getLogger(__name__)
 
 if TYPE_CHECKING:
@@ -33,6 +35,11 @@
         Set[achallenges.AnnotatedChallenge]
     ]
 
+_KeyAndCert = Union[
+    Tuple[crypto.PKey, crypto.X509],
+    Tuple[types.CertificateIssuerPrivateKeyTypes, x509.Certificate],
+]
+
 
 class ServerManager:
     """Standalone servers manager.
@@ -47,7 +54,7 @@
     will serve the same URLs!
 
     """
-    def __init__(self, certs: Mapping[bytes, Tuple[crypto.PKey, crypto.X509]],
+    def __init__(self, certs: Mapping[bytes, _KeyAndCert],
                  http_01_resources: Set[acme_standalone.HTTP01RequestHandler.HTTP01Resource]
                  ) -> None:
         self._instances: Dict[int, acme_standalone.HTTP01DualNetworkedServers] = {}
@@ -78,7 +85,7 @@
         try:
             servers = acme_standalone.HTTP01DualNetworkedServers(
                 address, self.http_01_resources)
-        except socket.error as error:
+        except OSError as error:
             raise errors.StandaloneBindError(error, port)
 
         servers.serve_forever()
@@ -137,7 +144,7 @@
         # values, main thread writes). Due to the nature of CPython's
         # GIL, the operations are safe, c.f.
         # https://docs.python.org/2/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe
-        self.certs: Mapping[bytes, Tuple[crypto.PKey, crypto.X509]] = {}
+        self.certs: Mapping[bytes, _KeyAndCert] = {}
         self.http_01_resources: Set[acme_standalone.HTTP01RequestHandler.HTTP01Resource] = set()
 
         self.servers = ServerManager(self.certs, self.http_01_resources)
diff -Nru python-certbot-2.11.0/certbot/_internal/plugins/webroot.py python-certbot-4.0.0/certbot/_internal/plugins/webroot.py
--- python-certbot-2.11.0/certbot/_internal/plugins/webroot.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/plugins/webroot.py	2025-04-07 18:03:33.000000000 -0400
@@ -58,7 +58,7 @@
 
     description = """\
 Saves the necessary validation files to a .well-known/acme-challenge/ directory within the \
-nominated webroot path. A seperate HTTP server must be running and serving files from the \
+nominated webroot path. A separate HTTP server must be running and serving files from the \
 webroot path. HTTP challenge only (wildcards not supported)."""
 
     MORE_INFO = """\
diff -Nru python-certbot-2.11.0/certbot/_internal/renewal.py python-certbot-4.0.0/certbot/_internal/renewal.py
--- python-certbot-2.11.0/certbot/_internal/renewal.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/renewal.py	2025-04-07 18:03:33.000000000 -0400
@@ -74,7 +74,7 @@
     """
     try:
         renewal_candidate = storage.RenewableCert(full_path, config)
-    except (errors.CertStorageError, IOError) as error:
+    except (OSError, errors.CertStorageError) as error:
         logger.error("Renewal configuration file %s is broken.", full_path)
         logger.error("The error was: %s\nSkipping.", str(error))
         logger.debug("Traceback was:\n%s", traceback.format_exc())
@@ -568,8 +568,6 @@
         raise errors.Error(
             f"{len(renew_failures)} renew failure(s), {len(parse_failures)} parse failure(s)")
 
-    # Windows installer integration tests rely on handle_renewal_request behavior here.
-    # If the text below changes, these tests will need to be updated accordingly.
     logger.debug("no renewal failures")
 
     return (renewed_domains, failed_domains)
diff -Nru python-certbot-2.11.0/certbot/_internal/snap_config.py python-certbot-4.0.0/certbot/_internal/snap_config.py
--- python-certbot-2.11.0/certbot/_internal/snap_config.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/snap_config.py	2025-04-07 18:03:33.000000000 -0400
@@ -1,4 +1,5 @@
 """Module configuring Certbot in a snap environment"""
+from __future__ import annotations
 import logging
 import socket
 from typing import Iterable
@@ -33,6 +34,7 @@
     'amd64': 'x86_64-linux-gnu',
     's390x': 's390x-linux-gnu',
 }
+CURRENT_PYTHON_VERSION_STRING = 'python3.12'
 
 LOGGER = logging.getLogger(__name__)
 
@@ -71,10 +73,35 @@
             raise e
 
     data = response.json()
-    connections = ['/snap/{0}/current/lib/python3.8/site-packages/'.format(item['slot']['snap'])
-                   for item in data.get('result', {}).get('established', [])
-                   if item.get('plug', {}).get('plug') == 'plugin'
-                   and item.get('plug-attrs', {}).get('content') == 'certbot-1']
+    connections = []
+    outdated_plugins = []
+    for plugin in data.get('result', {}).get('established', []):
+        plug: str = plugin.get('plug', {}).get('plug')
+        plug_content: str = plugin.get('plug-attrs', {}).get('content')
+        if plug == 'plugin' and plug_content == 'certbot-1':
+            plugin_name: str = plugin['slot']['snap']
+            # First, check that the plugin is using our expected python version,
+            # i.e. its "read" slot is something like
+            # "$SNAP/lib/python3.12/site-packages". If not, skip it and print an
+            # error.
+            slot_read: str = plugin.get('slot-attrs', {}).get('read', [])
+            if len(slot_read) != 0 and CURRENT_PYTHON_VERSION_STRING not in slot_read[0]:
+                outdated_plugins.append(plugin_name)
+                continue
+
+            connections.append('/snap/{0}/current/lib/{1}/site-packages/'.format(
+                plugin_name,
+                CURRENT_PYTHON_VERSION_STRING
+            ))
+
+    if outdated_plugins:
+        LOGGER.warning('The following plugins are using an outdated python version and must be '
+                    'updated to be compatible with Certbot 3.0. Please see '
+                    'https://community.letsencrypt.org/t/'
+                    'certbot-3-0-could-have-potential-third-party-snap-breakages/226940 '
+                    'for more information:')
+        plugin_list = '\n'.join('  * {}'.format(plugin) for plugin in outdated_plugins)
+        LOGGER.warning(plugin_list)
 
     os.environ['CERTBOT_PLUGIN_PATH'] = ':'.join(connections)
 
@@ -108,12 +135,12 @@
     # help out those packagers while ensuring this code works reliably, we offer custom versions of
     # both functions for now. when certbot does declare a dependency on requests>=2.32.2 in its
     # setup.py files, get_connection can be deleted
-    def get_connection(self, url: str,
+    def get_connection(self, url: str | bytes,
                        proxies: Optional[Iterable[str]] = None) -> _SnapdConnectionPool:
         return _SnapdConnectionPool()
 
     def get_connection_with_tls_context(self, request: PreparedRequest,
-                                        verify: bool,
+                                        verify: bool | str | None,
                                         proxies: Optional[Iterable[str]] = None,
                                         cert: Optional[Union[str, Tuple[str,str]]] = None
                                         ) -> _SnapdConnectionPool:
diff -Nru python-certbot-2.11.0/certbot/_internal/storage.py python-certbot-4.0.0/certbot/_internal/storage.py
--- python-certbot-2.11.0/certbot/_internal/storage.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/storage.py	2025-04-07 18:03:33.000000000 -0400
@@ -455,8 +455,7 @@
         renewal configuration file and/or systemwide defaults.
 
     """
-    def __init__(self, config_filename: str, cli_config: configuration.NamespaceConfig,
-                 update_symlinks: bool = False) -> None:
+    def __init__(self, config_filename: str, cli_config: configuration.NamespaceConfig) -> None:
         """Instantiate a RenewableCert object from an existing lineage.
 
         :param str config_filename: the path to the renewal config file
@@ -505,8 +504,6 @@
         self.live_dir = os.path.dirname(self.cert)
 
         self._fix_symlinks()
-        if update_symlinks:
-            self._update_symlinks()
         self._check_symlinks()
 
     @property
@@ -593,17 +590,6 @@
                 raise errors.CertStorageError("target {0} of symlink {1} does "
                                               "not exist".format(target, link))
 
-    def _update_symlinks(self) -> None:
-        """Updates symlinks to use archive_dir"""
-        for kind in ALL_FOUR:
-            link = getattr(self, kind)
-            previous_link = get_link_target(link)
-            new_link = os.path.join(self.relative_archive_dir(link),
-                os.path.basename(previous_link))
-
-            os.unlink(link)
-            os.symlink(new_link, link)
-
     def _consistent(self) -> bool:
         """Are the files associated with this lineage self-consistent?
 
@@ -636,10 +622,7 @@
                              "cert lineage's directory within the "
                              "official archive directory. Link: %s, "
                              "target directory: %s, "
-                             "archive directory: %s. If you've specified "
-                             "the archive directory in the renewal configuration "
-                             "file, you may need to update links by running "
-                             "certbot update_symlinks.",
+                             "archive directory: %s.",
                              link, os.path.dirname(target), self.archive_dir)
                 return False
 
@@ -1018,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
@@ -1108,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 -Nru python-certbot-2.11.0/certbot/_internal/tests/account_test.py python-certbot-4.0.0/certbot/_internal/tests/account_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/account_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/account_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -1,6 +1,5 @@
 """Tests for certbot._internal.account."""
 import datetime
-import json
 import sys
 import unittest
 from unittest import mock
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/cert_manager_test.py python-certbot-4.0.0/certbot/_internal/tests/cert_manager_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/cert_manager_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/cert_manager_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -65,44 +65,6 @@
         return config_file
 
 
-class UpdateLiveSymlinksTest(BaseCertManagerTest):
-    """Tests for certbot._internal.cert_manager.update_live_symlinks
-    """
-    def test_update_live_symlinks(self):
-        """Test update_live_symlinks"""
-        # create files with incorrect symlinks
-        from certbot._internal import cert_manager
-        archive_paths = {}
-        for domain in self.domains:
-            custom_archive = self.domains[domain]
-            if custom_archive is not None:
-                archive_dir_path = custom_archive
-            else:
-                archive_dir_path = os.path.join(self.config.default_archive_dir, domain)
-            archive_paths[domain] = {kind:
-                os.path.join(archive_dir_path, kind + "1.pem") for kind in ALL_FOUR}
-            for kind in ALL_FOUR:
-                live_path = self.config_files[domain][kind]
-                archive_path = archive_paths[domain][kind]
-                open(archive_path, 'a').close()
-                # path is incorrect but base must be correct
-                os.symlink(os.path.join(self.config.config_dir, kind + "1.pem"), live_path)
-
-        # run update symlinks
-        cert_manager.update_live_symlinks(self.config)
-
-        # check that symlinks go where they should
-        prev_dir = os.getcwd()
-        try:
-            for domain in self.domains:
-                for kind in ALL_FOUR:
-                    os.chdir(os.path.dirname(self.config_files[domain][kind]))
-                    assert filesystem.realpath(filesystem.readlink(self.config_files[domain][kind])) == \
-                        filesystem.realpath(archive_paths[domain][kind])
-        finally:
-            os.chdir(prev_dir)
-
-
 class DeleteTest(storage_test.BaseRenewableCertTest):
     """Tests for certbot._internal.cert_manager.delete
     """
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/client_test.py python-certbot-4.0.0/certbot/_internal/tests/client_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/client_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/client_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -158,9 +158,10 @@
                 with pytest.raises(errors.Error):
                     self._call()
 
-    def test_needs_email(self):
+    def test_no_email_is_chill(self):
         self.config.email = None
-        with pytest.raises(errors.Error):
+        with self._patched_acme_client() as mock_client:
+            mock_client().external_account_required.side_effect = self._false_mock
             self._call()
 
     @mock.patch("certbot._internal.client.logger")
@@ -172,7 +173,6 @@
                 self.config.register_unsafely_without_email = True
                 self.config.dry_run = False
                 self._call()
-                mock_logger.debug.assert_called_once_with(mock.ANY)
                 assert mock_prepare.called is True
 
     @mock.patch("certbot._internal.client.display_ops.get_email")
@@ -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)
@@ -646,6 +732,35 @@
                                             " every domain. The dry run will continue, but results"
                                             " may not be accurate.")
 
+    @mock.patch("certbot._internal.client.crypto_util")
+    def test_obtain_certificate_reuse_key_with_allow_subset_of_names(self, mock_crypto_util):
+        csr = util.CSR(form="pem", file=mock.sentinel.csr_file, data=CSR_SAN)
+        old_key = util.Key(file="old_key_file", pem="old_key_pem")
+        new_key = util.Key(file="new_key_file", pem="new_key_pem")
+        mock_crypto_util.generate_csr.return_value = csr
+        mock_crypto_util.generate_key.return_value = new_key
+        self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)
+
+        authzr = self._authzr_from_domains(["example.com"])
+        self.config.allow_subset_of_names = True
+        self.config.reuse_key = True
+
+        self._mock_obtain_certificate()
+
+        self.eg_order.authorizations = authzr
+        self.client.auth_handler.handle_authorizations.return_value = authzr
+
+        with test_util.patch_display_util():
+            mocked_open = mock.mock_open(read_data="old_key_pem")
+            with mock.patch('builtins.open', mocked_open):
+                result = self.client.obtain_certificate(self.eg_domains, "old_key_file")
+
+        assert result == \
+            (mock.sentinel.cert, mock.sentinel.chain, old_key, csr)
+        self._check_obtain_certificate(2)
+
+        assert mock_crypto_util.generate_key.call_count == 0
+
     def _set_mock_from_fullchain(self, mock_from_fullchain):
         mock_cert = mock.Mock()
         mock_cert.encode.return_value = mock.sentinel.cert
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/cli_test.py python-certbot-4.0.0/certbot/_internal/tests/cli_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/cli_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/cli_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -594,7 +594,7 @@
         assert_set_by_user_with_value(namespace, 'text_mode', True)
         assert_set_by_user_with_value(namespace, 'verbose_count', 1)
         assert_set_by_user_with_value(namespace, 'email', 'foo@example.com')
-    
+
     def test_arg_with_contained_spaces(self):
         # This can happen if a user specifies an arg like "-d foo.com" enclosed
         # in double quotes, or as its own line in a docker-compose.yml file (as
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/compat/filesystem_test.py python-certbot-4.0.0/certbot/_internal/tests/compat/filesystem_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/compat/filesystem_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/compat/filesystem_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -631,11 +631,11 @@
     @unittest.skipIf(POSIX_MODE, reason='Tests specific to Windows')
     @mock.patch("certbot.compat.filesystem.os.readlink")
     def test_normal_path_windows(self, mock_readlink):
-        # Python <3.8
+        # test the standard path format
         mock_readlink.return_value = "C:\\short\\path"
         assert filesystem.readlink("dummy") == "C:\\short\\path"
 
-        # Python >=3.8 (os.readlink always returns the extended form)
+        # test the extended form
         mock_readlink.return_value = "\\\\?\\C:\\short\\path"
         assert filesystem.readlink("dummy") == "C:\\short\\path"
 
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/configuration_test.py python-certbot-4.0.0/certbot/_internal/tests/configuration_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/configuration_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/configuration_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -1,15 +1,11 @@
 """Tests for certbot.configuration."""
 import sys
-import unittest
 from unittest import mock
-import warnings
 
 import pytest
 
 from certbot import errors
-from certbot._internal import cli
 from certbot._internal import constants
-from certbot._internal.plugins import disco
 from certbot.compat import misc
 from certbot.compat import os
 from certbot.tests import util as test_util
@@ -48,7 +44,6 @@
     def test_dynamic_dirs(self, mock_constants):
         mock_constants.ACCOUNTS_DIR = 'acc'
         mock_constants.BACKUP_DIR = 'backups'
-        mock_constants.CSR_DIR = 'csr'
 
         mock_constants.IN_PROGRESS_DIR = '../p'
         mock_constants.KEY_DIR = 'keys'
@@ -60,12 +55,6 @@
             os.path.normpath(os.path.join(self.config.config_dir, ref_path))
         assert os.path.normpath(self.config.backup_dir) == \
             os.path.normpath(os.path.join(self.config.work_dir, 'backups'))
-        with warnings.catch_warnings():
-            warnings.simplefilter("ignore", DeprecationWarning)
-            assert os.path.normpath(self.config.csr_dir) == \
-                os.path.normpath(os.path.join(self.config.config_dir, 'csr'))
-            assert os.path.normpath(self.config.key_dir) == \
-                os.path.normpath(os.path.join(self.config.config_dir, 'keys'))
         assert os.path.normpath(self.config.in_progress_dir) == \
             os.path.normpath(os.path.join(self.config.work_dir, '../p'))
         assert os.path.normpath(self.config.temp_checkpoint_dir) == \
@@ -100,10 +89,6 @@
                          os.path.join(os.getcwd(), logs_base)
         assert os.path.isabs(config.accounts_dir)
         assert os.path.isabs(config.backup_dir)
-        with warnings.catch_warnings():
-            warnings.simplefilter("ignore", DeprecationWarning)
-            assert os.path.isabs(config.csr_dir)
-            assert os.path.isabs(config.key_dir)
         assert os.path.isabs(config.in_progress_dir)
         assert os.path.isabs(config.temp_checkpoint_dir)
 
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/crypto_util_test.py python-certbot-4.0.0/certbot/_internal/tests/crypto_util_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/crypto_util_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/crypto_util_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -5,24 +5,29 @@
 import unittest
 from unittest import mock
 
-import OpenSSL
 import pytest
+from cryptography import x509
+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
 from certbot.compat import os
 import certbot.tests.util as test_util
 
-RSA256_KEY = test_util.load_vector('rsa256_key.pem')
-RSA256_KEY_PATH = test_util.vector_path('rsa256_key.pem')
 RSA512_KEY = test_util.load_vector('rsa512_key.pem')
+RSA512_KEY_PATH = test_util.vector_path('rsa512_key.pem')
+RSA2048_KEY = test_util.load_vector('rsa2048_key.pem')
 RSA2048_KEY_PATH = test_util.vector_path('rsa2048_key.pem')
 CERT_PATH = test_util.vector_path('cert_512.pem')
 CERT = test_util.load_vector('cert_512.pem')
 SS_CERT_PATH = test_util.vector_path('cert_2048.pem')
 SS_CERT = test_util.load_vector('cert_2048.pem')
 P256_KEY = test_util.load_vector('nistp256_key.pem')
+P256_KEY_PATH = test_util.vector_path('nistp256_key.pem')
 P256_CERT_PATH = test_util.vector_path('cert-nosans_nistp256.pem')
 P256_CERT = test_util.load_vector('cert-nosans_nistp256.pem')
 # CERT_LEAF is signed by CERT_ISSUER. CERT_ALT_ISSUER is a cross-sign of CERT_ISSUER.
@@ -120,7 +125,7 @@
 
     def test_invalid_false(self):
         assert not self._call(
-            test_util.load_vector('csr_512.pem'), RSA256_KEY)
+            test_util.load_vector('csr_512.pem'), P256_KEY)
 
 
 class ImportCSRFileTest(unittest.TestCase):
@@ -136,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"),
@@ -147,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"),
@@ -168,18 +173,19 @@
         from certbot.crypto_util import make_key
 
         # Do not test larger keys as it takes too long.
-        OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, make_key(2048))
+        serialization.load_pem_private_key(make_key(2048), password=None)
 
     def test_ec(self):  # pylint: disable=no-self-use
         # ECDSA Key Type Tests
         from certbot.crypto_util import make_key
 
         for (name, bits) in [('secp256r1', 256), ('secp384r1', 384), ('secp521r1', 521)]:
-            pkey = OpenSSL.crypto.load_privatekey(
-                OpenSSL.crypto.FILETYPE_PEM,
-                make_key(elliptic_curve=name, key_type='ecdsa')
+            pkey = serialization.load_pem_private_key(
+                make_key(elliptic_curve=name, key_type='ecdsa'),
+                password=None
             )
-            assert pkey.bits() == bits
+            assert isinstance(pkey, ec.EllipticCurvePrivateKey)
+            assert pkey.curve.key_size == bits
 
     def test_bad_key_sizes(self):
         from certbot.crypto_util import make_key
@@ -199,8 +205,15 @@
         # Try a bad --key-type
         with pytest.raises(errors.Error,
                            match=re.escape('Invalid key_type specified: unf.  Use [rsa|ecdsa]')):
-            OpenSSL.crypto.load_privatekey(
-                OpenSSL.crypto.FILETYPE_PEM, make_key(2048, key_type='unf'))
+            make_key(2048, key_type='unf')
+
+    def test_for_pkcs8_format(self):
+        from certbot.crypto_util import make_key
+
+        # PKCS#1 format will instead have text like "BEGIN RSA PRIVATE KEY" or "BEGIN EC PRIVATE
+        # KEY"
+        assert b"BEGIN PRIVATE KEY" in make_key(2048)
+        assert b"BEGIN PRIVATE KEY" in make_key(elliptic_curve='secp256r1', key_type='ecdsa')
 
 
 class VerifyCertSetup(unittest.TestCase):
@@ -291,7 +304,7 @@
         assert self._call(self.renewable_cert) is None
 
     def test_cert_priv_key_mismatch(self):
-        self.bad_renewable_cert.privkey = RSA256_KEY_PATH
+        self.bad_renewable_cert.privkey = P256_KEY_PATH
         self.bad_renewable_cert.cert = SS_CERT_PATH
 
         with pytest.raises(errors.Error):
@@ -355,8 +368,8 @@
             self._call(test_util.load_vector('cert-5sans_512.pem'))
 
     def test_parse_non_cert(self):
-        with pytest.raises(OpenSSL.crypto.Error):
-            self._call("hello there")
+        with pytest.raises(ValueError):
+            self._call(b"hello there")
 
 
 class GetNamesFromReqTest(unittest.TestCase):
@@ -381,26 +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
-
-        cert, file_type = pyopenssl_load_certificate(CERT)
-        assert cert.digest('sha256') == \
-                         OpenSSL.crypto.load_certificate(file_type, CERT).digest('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):
-            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):
@@ -432,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 -Nru python-certbot-2.11.0/certbot/_internal/tests/display/ops_test.py python-certbot-4.0.0/certbot/_internal/tests/display/ops_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/display/ops_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/display/ops_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -35,7 +35,7 @@
         with pytest.raises(errors.Error):
             self._call()
         with pytest.raises(errors.Error):
-            self._call(optional=False)
+            self._call()
 
     @test_util.patch_display_util()
     def test_ok_safe(self, mock_get_utility):
@@ -55,7 +55,7 @@
 
     @test_util.patch_display_util()
     def test_invalid_flag(self, mock_get_utility):
-        invalid_txt = "There seem to be problems"
+        invalid_txt = "The server reported a problem"
         mock_input = mock_get_utility().input
         mock_input.return_value = (display_util.OK, "foo@bar.baz")
         with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
@@ -66,18 +66,8 @@
             assert invalid_txt in mock_input.call_args[0][0]
 
     @test_util.patch_display_util()
-    def test_optional_flag(self, mock_get_utility):
-        mock_input = mock_get_utility().input
-        mock_input.return_value = (display_util.OK, "foo@bar.baz")
-        with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
-            mock_safe_email.side_effect = [False, True]
-            self._call(optional=False)
-            for call in mock_input.call_args_list:
-                assert "--register-unsafely-without-email" not in call[0][0]
-
-    @test_util.patch_display_util()
     def test_optional_invalid_unsafe(self, mock_get_utility):
-        invalid_txt = "There seem to be problems"
+        invalid_txt = "There is a problem"
         mock_input = mock_get_utility().input
         mock_input.return_value = (display_util.OK, "foo@bar.baz")
         with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/display/util_test.py python-certbot-4.0.0/certbot/_internal/tests/display/util_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/display/util_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/display/util_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -1,13 +1,8 @@
 """Test :mod:`certbot.display.util`."""
-import io
-import socket
 import sys
-import tempfile
-from unittest import mock
 
 import pytest
 
-from certbot import errors
 import certbot.tests.util as test_util
 
 
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/hook_test.py python-certbot-4.0.0/certbot/_internal/tests/hook_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/hook_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/hook_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -136,7 +136,8 @@
 
     def _test_nonrenew_common(self):
         mock_execute = self._call_with_mock_execute(self.config)
-        mock_execute.assert_called_once_with("pre-hook", self.config.pre_hook, env=mock.ANY)
+        mock_execute.assert_any_call("pre-hook", self.dir_hook, env=mock.ANY)
+        mock_execute.assert_called_with("pre-hook", self.config.pre_hook, env=mock.ANY)
         self._test_no_executions_common()
 
     def test_no_hooks(self):
@@ -208,17 +209,18 @@
         for verb in ("certonly", "run",):
             self.config.verb = verb
             mock_execute = self._call_with_mock_execute(self.config, [])
-            mock_execute.assert_called_once_with("post-hook", self.config.post_hook, env=mock.ANY)
+            mock_execute.assert_any_call("post-hook", self.dir_hook, env=mock.ANY)
+            mock_execute.assert_called_with("post-hook", self.config.post_hook, env=mock.ANY)
             assert not self._get_eventually()
 
-    def test_cert_only_and_run_without_hook(self):
+    def test_certonly_and_run_without_cli_hook(self):
         self.config.post_hook = None
         for verb in ("certonly", "run",):
             self.config.verb = verb
-            assert not self._call_with_mock_execute(self.config, []).called
+            mock_execute = self._call_with_mock_execute(self.config, [])
+            mock_execute.assert_called_once_with("post-hook", self.dir_hook, env=mock.ANY)
             assert not self._get_eventually()
 
-    @unittest.skipIf(pyver_lt(3, 8), "Python 3.8+ required for this test.")
     def test_renew_env(self):
         self.config.verb = "certonly"
         args = self._call_with_mock_execute(self.config, ["success.org"]).call_args
@@ -308,7 +310,6 @@
         mock_execute = self._call_with_mock_execute_and_eventually([], [])
         mock_execute.assert_called_once_with("post-hook", self.eventually[0], env=mock.ANY)
 
-    @unittest.skipIf(pyver_lt(3, 8), "Python 3.8+ required for this test.")
     def test_env(self):
         self.eventually = ["foo"]
         mock_execute = self._call_with_mock_execute_and_eventually(["success.org"], ["failed.org"])
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/lock_test.py python-certbot-4.0.0/certbot/_internal/tests/lock_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/lock_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/lock_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -115,10 +115,10 @@
             mocked_function = 'certbot._internal.lock.msvcrt.locking'
         msg = 'hi there'
         with mock.patch(mocked_function) as mock_lock:
-            mock_lock.side_effect = IOError(msg)
+            mock_lock.side_effect = OSError(msg)
             try:
                 self._call(self.lock_path)
-            except IOError as err:
+            except OSError as err:
                 assert msg in str(err)
             else:  # pragma: no cover
                 self.fail('IOError not raised')
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/main_test.py python-certbot-4.0.0/certbot/_internal/tests/main_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/main_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/main_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -16,12 +16,12 @@
 from unittest import mock
 
 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
@@ -42,9 +42,9 @@
 CERT_PATH = test_util.vector_path('cert_512.pem')
 CERT = test_util.vector_path('cert_512.pem')
 CSR = test_util.vector_path('csr_512.der')
-KEY = test_util.vector_path('rsa256_key.pem')
 JWK = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
 RSA2048_KEY_PATH = test_util.vector_path('rsa2048_key.pem')
+P256_KEY_PATH = test_util.vector_path('p256_key.pem')
 SS_CERT_PATH = test_util.vector_path('cert_2048.pem')
 
 
@@ -218,6 +218,7 @@
         with pytest.raises(errors.NotSupportedError):
             main.run(self.config, plugins)
 
+
 class CertonlyTest(unittest.TestCase):
     """Tests for certbot._internal.main.certonly."""
 
@@ -264,7 +265,7 @@
         mock_domains.return_value = domains
         mock_lineage.names.return_value = domains
         self._call(('certonly --webroot -d example.com -d test.org '
-            '--cert-name example.com').split())
+            '--cert-name example.com --no-directory-hooks').split())
 
         assert mock_lineage.call_count == 1
         assert mock_domains.call_count == 1
@@ -276,7 +277,7 @@
 
         # user confirms updating lineage with new domains
         self._call(('certonly --webroot -d example.com -d test.com '
-            '--cert-name example.com').split())
+            '--cert-name example.com --no-directory-hooks').split())
         assert mock_lineage.call_count == 2
         assert mock_domains.call_count == 2
         assert mock_renew_cert.call_count == 2
@@ -286,7 +287,8 @@
         # error in _ask_user_to_confirm_new_names
         self.mock_get_utility().yesno.return_value = False
         with pytest.raises(errors.ConfigurationError):
-            self._call('certonly --webroot -d example.com -d test.com --cert-name example.com'.split())
+            self._call('certonly --webroot -d example.com -d test.com --cert-name example.com'
+                ' --no-directory-hooks'.split())
 
     @mock.patch('certbot._internal.main._report_next_steps')
     @mock.patch('certbot._internal.cert_manager.domains_for_certname')
@@ -299,14 +301,14 @@
 
         # no lineage with this name but we specified domains so create a new cert
         self._call(('certonly --webroot -d example.com -d test.com '
-            '--cert-name example.com').split())
+            '--cert-name example.com --no-directory-hooks').split())
         assert mock_lineage.call_count == 1
         assert mock_report_cert.call_count == 1
 
         # no lineage with this name and we didn't give domains
         mock_choose_names.return_value = ["somename"]
         mock_domains_for_certname.return_value = None
-        self._call(('certonly --webroot --cert-name example.com').split())
+        self._call(('certonly --webroot --cert-name example.com --no-directory-hooks').split())
         assert mock_choose_names.called is True
 
     @mock.patch('certbot._internal.main._report_next_steps')
@@ -659,7 +661,7 @@
 
         # new account
         try:
-            self._call(f'--cert-name example.com --account newaccountid'.split())
+            self._call('--cert-name example.com --account newaccountid'.split())
         except errors.ConfigurationError as err:
             assert "Using reconfigure to change the ACME account" in str(err)
 
@@ -674,7 +676,7 @@
 
         # new server
         try:
-            self._call(f'--cert-name example.com --server x.com'.split())
+            self._call('--cert-name example.com --server x.com'.split())
         except errors.ConfigurationError as err:
             assert "Using reconfigure to change the ACME account" in str(err)
 
@@ -1055,8 +1057,6 @@
     def test_noninteractive(self, _):
         args = ['-n', 'certonly']
         self._cli_missing_flag(args, "specify a plugin")
-        args.extend(['--standalone', '-d', 'eg.is'])
-        self._cli_missing_flag(args, "register before running")
 
     @mock.patch('certbot._internal.eff.handle_subscription')
     @mock.patch('certbot._internal.log.post_arg_parse_setup')
@@ -1215,11 +1215,6 @@
         client.rollback.assert_called_once_with(
             mock.ANY, 123, mock.ANY, mock.ANY)
 
-    @mock.patch('certbot._internal.cert_manager.update_live_symlinks')
-    def test_update_symlinks(self, mock_cert_manager):
-        self._call_no_clientmock(['update_symlinks'])
-        assert 1 == mock_cert_manager.call_count
-
     @mock.patch('certbot._internal.cert_manager.certificates')
     def test_certificates(self, mock_cert_manager):
         self._call_no_clientmock(['certificates'])
@@ -1332,7 +1327,7 @@
     def test_certonly_bad_args(self):
         try:
             self._call(['-a', 'bad_auth', 'certonly'])
-            assert False, "Exception should have been raised"
+            pytest.fail("Exception should have been raised")
         except errors.PluginSelectionError as e:
             assert 'The requested bad_auth plugin does not appear' in str(e)
 
@@ -1361,7 +1356,7 @@
         except errors.Error as e:
             assert "Please try the certonly" in repr(e)
             return
-        assert False, "Expected supplying --csr to fail with default verb"
+        pytest.fail("Expected supplying --csr to fail with default verb")
 
     def test_csr_with_no_domains(self):
         with pytest.raises(errors.Error):
@@ -1415,14 +1410,6 @@
         assert 'donate' in mock_register.call_args[0][1]
         assert mock_subscription.called is True
 
-    @mock.patch('certbot._internal.eff.handle_subscription')
-    def test_certonly_new_request_failure(self, mock_subscription):
-        mock_client = mock.MagicMock()
-        mock_client.obtain_and_enroll_certificate.return_value = False
-        with pytest.raises(errors.Error):
-            self._certonly_new_request_common(mock_client)
-        assert mock_subscription.called is False
-
     def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None,
                              args=None, should_renew=True, error_expected=False,
                              quiet_mode=False, expiry_date=datetime.datetime.now(),
@@ -1816,16 +1803,14 @@
             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]
+            cert = x509.load_pem_x509_certificate(f.read())
             mock_revoke = mock_acme_client.ClientV2().revoke
-            mock_revoke.assert_called_once_with(
-                    jose.ComparableX509(cert),
-                    mock.ANY)
+            mock_revoke.assert_called_once_with(cert, mock.ANY)
 
     def test_revoke_with_key_mismatch(self):
         server = 'foo.bar'
         with pytest.raises(errors.Error):
-            self._call_no_clientmock(['--cert-path', CERT, '--key-path', KEY,
+            self._call_no_clientmock(['--cert-path', CERT, '--key-path', P256_KEY_PATH,
                                  '--server', server, 'revoke'])
 
     @mock.patch('certbot._internal.main._delete_if_appropriate')
@@ -1835,12 +1820,10 @@
         mock_delete_if_appropriate.return_value = False
         mock_determine_account.return_value = (mock.MagicMock(), None)
         _, _, _, client = self._call(['--cert-path', CERT, 'revoke'])
-        with open(CERT) as f:
-            cert = crypto_util.pyopenssl_load_certificate(f.read())[0]
+        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(
-                    jose.ComparableX509(cert),
-                    mock.ANY)
+            mock_revoke.assert_called_once_with(cert, mock.ANY)
 
     @mock.patch('certbot._internal.log.post_arg_parse_setup')
     def test_register(self, _):
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/ocsp_test.py python-certbot-4.0.0/certbot/_internal/tests/ocsp_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/ocsp_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/ocsp_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -324,8 +324,8 @@
         responder_name=responder.subject,
         certificates=[responder],
         hash_algorithm=hashes.SHA1(),
-        next_update=datetime.now(pytz.UTC).replace(tzinfo=None) + timedelta(days=1),
-        this_update=datetime.now(pytz.UTC).replace(tzinfo=None) - timedelta(days=1),
+        next_update_utc=datetime.now(pytz.UTC) + timedelta(days=1),
+        this_update_utc=datetime.now(pytz.UTC) - timedelta(days=1),
         signature_algorithm_oid=x509.oid.SignatureAlgorithmOID.RSA_WITH_SHA1,
     )
 
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/plugins/enhancements_test.py python-certbot-4.0.0/certbot/_internal/tests/plugins/enhancements_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/plugins/enhancements_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/plugins/enhancements_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -1,6 +1,5 @@
 """Tests for new style enhancements"""
 import sys
-import unittest
 from unittest import mock
 
 import pytest
@@ -17,7 +16,6 @@
         super().setUp()
         self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement)
 
-
     @test_util.patch_display_util()
     def test_enhancement_enabled_enhancements(self, _):
         FAKEINDEX = [
@@ -57,8 +55,7 @@
         lineage = "lineage"
         enhancements.enable(lineage, domains, self.mockinstaller, self.config)
         assert self.mockinstaller.enable_autohsts.called
-        assert self.mockinstaller.enable_autohsts.call_args[0] == \
-                          (lineage, domains)
+        assert self.mockinstaller.enable_autohsts.call_args[0] == (lineage, domains)
 
 
 if __name__ == '__main__':
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/plugins/selection_test.py python-certbot-4.0.0/certbot/_internal/tests/plugins/selection_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/plugins/selection_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/plugins/selection_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -245,7 +245,7 @@
         inst, auth = self._runWithArgs("certonly --apache")
         assert inst.name == "apache"
         assert auth.name == "apache"
-    
+
     def test_noninteractive_inst_arg(self):
         # For certonly, if an installer arg is set, it should be returned as expected
         inst, auth = self._runWithArgs("certonly -a nginx -i nginx")
@@ -258,18 +258,19 @@
 
         # if no installer arg is set (or it's set to none), one shouldn't be returned
         inst, auth = self._runWithArgs("certonly -a nginx")
-        assert inst == None
+        assert inst is None
         assert auth.name == "nginx"
         inst, auth = self._runWithArgs("certonly -a nginx -i none")
-        assert inst == None
+        assert inst is None
         assert auth.name == "nginx"
 
         inst, auth = self._runWithArgs("certonly -a apache")
-        assert inst == None
+        assert inst is None
         assert auth.name == "apache"
         inst, auth = self._runWithArgs("certonly -a apache -i none")
-        assert inst == None
+        assert inst is None
         assert auth.name == "apache"
 
+
 if __name__ == "__main__":
     sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/plugins/standalone_test.py python-certbot-4.0.0/certbot/_internal/tests/plugins/standalone_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/plugins/standalone_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/plugins/standalone_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -9,7 +9,6 @@
 from unittest import mock
 
 import josepy as jose
-import OpenSSL.crypto
 import pytest
 
 from acme import challenges
@@ -24,8 +23,8 @@
     """Tests for certbot._internal.plugins.standalone.ServerManager."""
 
     def setUp(self):
-        from certbot._internal.plugins.standalone import ServerManager
-        self.certs: Dict[bytes, Tuple[OpenSSL.crypto.PKey, OpenSSL.crypto.X509]] = {}
+        from certbot._internal.plugins.standalone import ServerManager, _KeyAndCert
+        self.certs: Dict[bytes, _KeyAndCert] = {}
         self.http_01_resources: Set[acme_standalone.HTTP01RequestHandler.HTTP01Resource] = {}
         self.mgr = ServerManager(self.certs, self.http_01_resources)
 
@@ -59,7 +58,7 @@
         maybe_another_server = socket.socket()
         try:
             maybe_another_server.bind(("", port))
-        except socket.error:
+        except OSError:
             pass
         with pytest.raises(errors.StandaloneBindError):
             self.mgr.run(port,
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/plugins/storage_test.py python-certbot-4.0.0/certbot/_internal/tests/plugins/storage_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/plugins/storage_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/plugins/storage_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -1,10 +1,6 @@
 """Tests for certbot.plugins.storage.PluginStorage"""
 import json
 import sys
-from typing import Iterable
-from typing import List
-from typing import Optional
-import unittest
 from unittest import mock
 
 import pytest
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/plugins/webroot_test.py python-certbot-4.0.0/certbot/_internal/tests/plugins/webroot_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/plugins/webroot_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/plugins/webroot_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -190,7 +190,7 @@
             with open(permission_canary, "r"):
                 pass
             print("Warning, running tests as root skips permissions tests...")
-        except IOError:
+        except OSError:
             # ok, permissions work, test away...
             with pytest.raises(errors.PluginError):
                 self.auth.perform([])
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/reverter_test.py python-certbot-4.0.0/certbot/_internal/tests/reverter_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/reverter_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/reverter_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -65,7 +65,7 @@
 
     def test_add_to_checkpoint_copy_failure(self):
         with mock.patch("certbot.reverter.shutil.copy2") as mock_copy2:
-            mock_copy2.side_effect = IOError("bad copy")
+            mock_copy2.side_effect = OSError("bad copy")
             with pytest.raises(errors.ReverterError):
                 self.reverter.add_to_checkpoint(self.sets[0], "save1")
 
diff -Nru python-certbot-2.11.0/certbot/_internal/tests/storage_test.py python-certbot-4.0.0/certbot/_internal/tests/storage_test.py
--- python-certbot-2.11.0/certbot/_internal/tests/storage_test.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/_internal/tests/storage_test.py	2025-04-07 18:03:33.000000000 -0400
@@ -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
@@ -838,21 +898,6 @@
         assert stat.S_IMODE(os.lstat(temp).st_mode) == \
                          stat.S_IMODE(os.lstat(temp2).st_mode)
 
-    def test_update_symlinks(self):
-        from certbot._internal import storage
-        archive_dir_path = os.path.join(self.config.config_dir, "archive", "example.org")
-        for kind in ALL_FOUR:
-            live_path = self.config_file[kind]
-            basename = kind + "1.pem"
-            archive_path = os.path.join(archive_dir_path, basename)
-            open(archive_path, 'a').close()
-            os.symlink(os.path.join(self.config.config_dir, basename), live_path)
-        with pytest.raises(errors.CertStorageError):
-            storage.RenewableCert(self.config_file.filename,
-                          self.config)
-        storage.RenewableCert(self.config_file.filename, self.config,
-            update_symlinks=True)
-
     def test_truncate(self):
         # It should not do anything when there's less than 5 cert history
         for kind in ALL_FOUR:
diff -Nru python-certbot-2.11.0/certbot/ocsp.py python-certbot-4.0.0/certbot/ocsp.py
--- python-certbot-2.11.0/certbot/ocsp.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/ocsp.py	2025-04-07 18:03:33.000000000 -0400
@@ -234,12 +234,12 @@
     # See OpenSSL implementation as a reference:
     # https://github.com/openssl/openssl/blob/ef45aa14c5af024fcb8bef1c9007f3d1c115bd85/crypto/ocsp/ocsp_cl.c#L338-L391
     # thisUpdate/nextUpdate are expressed in UTC/GMT time zone
-    now = datetime.now(pytz.UTC).replace(tzinfo=None)
-    if not response_ocsp.this_update:
+    now = datetime.now(pytz.UTC)
+    if not response_ocsp.this_update_utc:
         raise AssertionError('param thisUpdate is not set.')
-    if response_ocsp.this_update > now + timedelta(minutes=5):
+    if response_ocsp.this_update_utc > now + timedelta(minutes=5):
         raise AssertionError('param thisUpdate is in the future.')
-    if response_ocsp.next_update and response_ocsp.next_update < now - timedelta(minutes=5):
+    if response_ocsp.next_update_utc and response_ocsp.next_update_utc < now - timedelta(minutes=5):
         raise AssertionError('param nextUpdate is in the past.')
 
 
diff -Nru python-certbot-2.11.0/certbot/plugins/common.py python-certbot-4.0.0/certbot/plugins/common.py
--- python-certbot-2.11.0/certbot/plugins/common.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/plugins/common.py	2025-04-07 18:03:33.000000000 -0400
@@ -2,10 +2,10 @@
 from abc import ABCMeta
 from abc import abstractmethod
 import argparse
+import importlib.resources
 import logging
 import re
 import shutil
-import sys
 import tempfile
 from typing import Any
 from typing import Callable
@@ -31,11 +31,6 @@
 from certbot.interfaces import Plugin as AbstractPlugin
 from certbot.plugins.storage import PluginStorage
 
-if sys.version_info >= (3, 9):  # pragma: no cover
-    import importlib.resources as importlib_resources
-else:  # pragma: no cover
-    import importlib_resources
-
 logger = logging.getLogger(__name__)
 
 
@@ -266,7 +261,7 @@
         self.ipv6 = ipv6
 
     @classmethod
-    def fromstring(cls: Type[GenericAddr], str_addr: str) -> Optional[GenericAddr]:
+    def fromstring(cls: Type[GenericAddr], str_addr: str) -> GenericAddr:
         """Initialize Addr from string."""
         if str_addr.startswith('['):
             # ipv6 addresses starts with [
@@ -465,8 +460,8 @@
     filesystem.chmod(config_dir, constants.CONFIG_DIRS_MODE)
     filesystem.chmod(work_dir, constants.CONFIG_DIRS_MODE)
 
-    test_dir_ref = importlib_resources.files(pkg).joinpath("testdata").joinpath(test_dir)
-    with importlib_resources.as_file(test_dir_ref) as path:
+    test_dir_ref = importlib.resources.files(pkg).joinpath("testdata").joinpath(test_dir)
+    with importlib.resources.as_file(test_dir_ref) as path:
         shutil.copytree(
             path, os.path.join(temp_dir, test_dir), symlinks=True)
 
diff -Nru python-certbot-2.11.0/certbot/plugins/dns_common.py python-certbot-4.0.0/certbot/plugins/dns_common.py
--- python-certbot-2.11.0/certbot/plugins/dns_common.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/plugins/dns_common.py	2025-04-07 18:03:33.000000000 -0400
@@ -25,6 +25,10 @@
 logger = logging.getLogger(__name__)
 
 
+# As of writing this, the only one of our plugins that does not inherit from this class (either
+# directly or indirectly through certbot.plugins.dns_common_lexicon.LexiconDNSAuthenticator) is
+# certbot-dns-route53. If you are attempting to make changes to all of our DNS plugins, please keep
+# this difference in mind.
 class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.ABCMeta):
     """Base class for DNS Authenticators"""
 
diff -Nru python-certbot-2.11.0/certbot/plugins/storage.py python-certbot-4.0.0/certbot/plugins/storage.py
--- python-certbot-2.11.0/certbot/plugins/storage.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/plugins/storage.py	2025-04-07 18:03:33.000000000 -0400
@@ -48,7 +48,7 @@
         try:
             with open(self._storagepath, 'r') as fh:
                 filedata = fh.read()
-        except IOError as e:
+        except OSError as e:
             errmsg = "Could not read PluginStorage data file: {0} : {1}".format(
                 self._storagepath, str(e))
             if os.path.isfile(self._storagepath):
@@ -92,7 +92,7 @@
                     os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
                     0o600), 'w') as fh:
                 fh.write(serialized)
-        except IOError as e:
+        except OSError as e:
             errmsg = "Could not write PluginStorage data to file {0} : {1}".format(
                 self._storagepath, str(e))
             logger.error(errmsg)
diff -Nru python-certbot-2.11.0/certbot/reverter.py python-certbot-4.0.0/certbot/reverter.py
--- python-certbot-2.11.0/certbot/reverter.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/reverter.py	2025-04-07 18:03:33.000000000 -0400
@@ -184,7 +184,7 @@
                         cp_dir, os.path.basename(filename) + "_" + str(idx)))
                     op_fd.write('{0}\n'.format(filename))
                 # https://stackoverflow.com/questions/4726260/effective-use-of-python-shutil-copy2
-                except IOError:
+                except OSError:
                     op_fd.close()
                     logger.error(
                         "Unable to add file %s to checkpoint %s",
@@ -238,7 +238,7 @@
                         shutil.copy2(os.path.join(
                             cp_dir,
                             os.path.basename(path) + "_" + str(idx)), path)
-            except (IOError, OSError):
+            except OSError:
                 # This file is required in all checkpoints.
                 logger.error("Unable to recover files from %s", cp_dir)
                 raise errors.ReverterError(f"Unable to recover files from {cp_dir}")
@@ -327,7 +327,7 @@
             for path in files:
                 if path not in ex_files:
                     new_fd.write("{0}\n".format(path))
-        except (IOError, OSError):
+        except OSError:
             logger.error("Unable to register file creation(s) - %s", files)
             raise errors.ReverterError(
                 "Unable to register file creation(s) - {0}".format(files))
@@ -360,7 +360,7 @@
             with open(commands_fp, mode, **kwargs) as f:  # type: ignore
                 csvwriter = csv.writer(f)
                 csvwriter.writerow(command)
-        except (IOError, OSError):
+        except OSError:
             logger.error("Unable to register undo command")
             raise errors.ReverterError(
                 "Unable to register undo command.")
@@ -434,7 +434,7 @@
                             "File: %s - Could not be found to be deleted\n"
                             " - Certbot probably shut down unexpectedly",
                             path)
-        except (IOError, OSError):
+        except OSError:
             logger.critical(
                 "Unable to remove filepaths contained within %s", file_list)
             raise errors.ReverterError(
@@ -476,7 +476,7 @@
 
         # Move self.config.in_progress_dir to Backups directory
             shutil.move(changes_since_tmp_path, changes_since_path)
-        except (IOError, OSError):
+        except OSError:
             logger.error("Unable to finalize checkpoint - adding title")
             logger.debug("Exception was:\n%s", traceback.format_exc())
             raise errors.ReverterError("Unable to add title")
diff -Nru python-certbot-2.11.0/certbot/tests/acme_util.py python-certbot-4.0.0/certbot/tests/acme_util.py
--- python-certbot-2.11.0/certbot/tests/acme_util.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/tests/acme_util.py	2025-04-07 18:03:33.000000000 -0400
@@ -12,7 +12,7 @@
 from certbot.tests import util
 
 JWK = jose.JWK.load(util.load_vector('rsa512_key.pem'))
-KEY = util.load_rsa_private_key('rsa512_key.pem')
+KEY = util.load_jose_rsa_private_key_pem('rsa512_key.pem')
 
 # Challenges
 HTTP01 = challenges.HTTP01(
diff -Nru python-certbot-2.11.0/certbot/tests/testdata/README python-certbot-4.0.0/certbot/tests/testdata/README
--- python-certbot-2.11.0/certbot/tests/testdata/README	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/tests/testdata/README	2025-04-07 18:03:33.000000000 -0400
@@ -1,6 +1,6 @@
 The following command has been used to generate test keys:
 
-	for x in 256 512 2048; do openssl genrsa -out rsa${k}_key.pem $k; done
+	for x in 512 2048; do openssl genrsa -out rsa${k}_key.pem $k; done
 
 For the elliptic curve private keys, this command was used:
 
@@ -15,3 +15,24 @@
 and for the certificate:
 
   openssl req -new -out cert_X.pem -key rsaX_key.pem -subj '/CN=example.com' -x509 [-outform DER > cert_X.der]
+
+`csr-mixed.pem` was generated with pyca/cryptography using the following snippet:
+
+	from cryptography import x509
+	from cryptography.hazmat.primitives import hashes, serialization
+	k = serialization.load_pem_private_key(
+	    open("./acme/acme/_internal/tests/testdata/rsa2048_key.pem", "rb").read(), None
+	)
+	csr = (
+	    x509.CertificateSigningRequestBuilder().add_extension(
+	        x509.SubjectAlternativeName([x509.DNSName('a.exemple.com'), x509.IPAddress(ipaddress.ipaddr('192.0.2.111'))]),
+	        critical=False
+	    ).subject_name(
+	        x509.Name([])
+	    ).sign(
+	        k, hashes.SHA256()
+	    )
+	)
+	open("./acme/acme/_internal/tests/testdata/csr-mixed.pem", "wb").write(
+	    csr.public_bytes(serialization.Encoding.PEM)
+	)
diff -Nru python-certbot-2.11.0/certbot/tests/testdata/rsa256_key.pem python-certbot-4.0.0/certbot/tests/testdata/rsa256_key.pem
--- python-certbot-2.11.0/certbot/tests/testdata/rsa256_key.pem	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/tests/testdata/rsa256_key.pem	1969-12-31 19:00:00.000000000 -0500
@@ -1,6 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-MIGrAgEAAiEAm2Fylv+Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEkCAwEAAQIh
-AJT0BA/xD01dFCAXzSNyj9nfSZa3NpqzJZZn/eOm7vghAhEAzUVNZn4lLLBD1R6N
-E8TKNQIRAMHHyn3O5JeY36lwKwkUlEUCEAliRauN0L0+QZuYjfJ9aJECEGx4dru3
-rTPCyighdqWNlHUCEQCiLjlwSRtWgmMBudCkVjzt
------END RSA PRIVATE KEY-----
diff -Nru python-certbot-2.11.0/certbot/tests/util.py python-certbot-4.0.0/certbot/tests/util.py
--- python-certbot-2.11.0/certbot/tests/util.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/tests/util.py	2025-04-07 18:03:33.000000000 -0400
@@ -3,6 +3,7 @@
 from contextlib import ExitStack
 import copy
 from importlib import reload as reload_module
+import importlib.resources
 import io
 import logging
 import multiprocessing
@@ -21,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
@@ -38,11 +40,6 @@
 from certbot.display import util as display_util
 from certbot.plugins import common
 
-if sys.version_info >= (3, 9):  # pragma: no cover
-    import importlib.resources as importlib_resources
-else:  # pragma: no cover
-    import importlib_resources
-
 
 class DummyInstaller(common.Installer):
     """Dummy installer plugin for test purpose."""
@@ -84,14 +81,14 @@
     """Path to a test vector."""
     _file_manager = ExitStack()
     atexit.register(_file_manager.close)
-    vector_ref = importlib_resources.files(__package__).joinpath('testdata', *names)
-    path = _file_manager.enter_context(importlib_resources.as_file(vector_ref))
+    vector_ref = importlib.resources.files(__package__).joinpath('testdata', *names)
+    path = _file_manager.enter_context(importlib.resources.as_file(vector_ref))
     return str(path)
 
 
 def load_vector(*names: str) -> bytes:
     """Load contents of a test vector."""
-    vector_ref = importlib_resources.files(__package__).joinpath('testdata', *names)
+    vector_ref = importlib.resources.files(__package__).joinpath('testdata', *names)
     data = vector_ref.read_bytes()
     # Try at most to convert CRLF to LF when data is text
     try:
@@ -102,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
@@ -111,43 +108,40 @@
     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))
+        names[-1], x509.load_pem_x509_certificate, x509.load_der_x509_certificate
+    )
+    return 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_jose_rsa_private_key_pem(*names: str) -> jose.ComparableRSAKey:
+    """Load RSA private key wrapped in jose.ComparableRSAKey"""
+    return jose.ComparableRSAKey(load_rsa_private_key_pem(*names))
 
 
-def load_comparable_csr(*names: str) -> jose.ComparableX509:
-    """Load ComparableX509 certificate request."""
-    return jose.ComparableX509(load_csr(*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(*names: str) -> jose.ComparableRSAKey:
+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
     else:
         loader_fn = serialization.load_der_private_key
-    return jose.ComparableRSAKey(
-        cast(RSAPrivateKey,
-             loader_fn(load_vector(*names), password=None, backend=default_backend())))
-
-
-def load_pyopenssl_private_key(*names: str) -> crypto.PKey:
-    """Load pyOpenSSL private key."""
-    loader = _guess_loader(
-        names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1)
-    return crypto.load_privatekey(loader, load_vector(*names))
+    key = loader_fn(load_vector(*names), password=None, backend=default_backend())
+    assert isinstance(key, RSAPrivateKey)
+    return key
 
 
 def make_lineage(config_dir: str, testfile: str, ec: bool = True) -> str:
@@ -453,7 +447,7 @@
     process.start()
 
     # Wait confirmation that lock is acquired
-    assert receive_event.wait(timeout=10), 'Timeout while waiting to acquire the lock.'
+    assert receive_event.wait(timeout=20), 'Timeout while waiting to acquire the lock.'
     # Execute the callback
     callback()
     # Trigger unlock from foreign process
diff -Nru python-certbot-2.11.0/certbot/util.py python-certbot-4.0.0/certbot/util.py
--- python-certbot-2.11.0/certbot/util.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/certbot/util.py	2025-04-07 18:03:33.000000000 -0400
@@ -165,6 +165,12 @@
     # Avoid accidentally modifying env
     if 'SNAP' not in env or 'CERTBOT_SNAPPED' not in env:
         return env
+
+    # These environment variables being set when running external programs can cause issues if these
+    # programs also use OpenSSL. See https://github.com/certbot/certbot/issues/10190.
+    env.pop('OPENSSL_FORCE_FIPS_MODE', None)
+    env.pop('OPENSSL_MODULES', None)
+
     for path_name in ('PATH', 'LD_LIBRARY_PATH'):
         if path_name in env:
             env[path_name] = ':'.join(x for x in env[path_name].split(':') if env['SNAP'] not in x)
@@ -407,7 +413,11 @@
 
     :returns: os_ua
     :rtype: `str`
+
     """
+    # distro.name returns an empty string if one cannot be determined. see
+    # https://github.com/python-distro/distro/blob/3bd19e61fcb7f8d2bf3d45d9e40d69c92e05d241/src/distro/distro.py#L883
+    os_info = ""
     if _USE_DISTRO:
         os_info = distro.name(pretty=True)
 
@@ -665,12 +675,12 @@
         socket.inet_pton(socket.AF_INET, address)
         # If this line runs it was ip address (ipv4)
         return True
-    except socket.error:
+    except OSError:
         # It wasn't an IPv4 address, so try ipv6
         try:
             socket.inet_pton(socket.AF_INET6, address)
             return True
-        except socket.error:
+        except OSError:
             return False
 
 
diff -Nru python-certbot-2.11.0/certbot.egg-info/PKG-INFO python-certbot-4.0.0/certbot.egg-info/PKG-INFO
--- python-certbot-2.11.0/certbot.egg-info/PKG-INFO	2024-06-05 17:34:03.000000000 -0400
+++ python-certbot-4.0.0/certbot.egg-info/PKG-INFO	2025-04-07 18:03:34.000000000 -0400
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: certbot
-Version: 2.11.0
+Version: 4.0.0
 Summary: ACME client
 Home-page: https://github.com/certbot/certbot
 Author: Certbot Project
@@ -14,37 +14,35 @@
 Classifier: Operating System :: POSIX :: Linux
 Classifier: Programming Language :: Python
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
 Classifier: Topic :: Internet :: WWW/HTTP
 Classifier: Topic :: Security
 Classifier: Topic :: System :: Installation/Setup
 Classifier: Topic :: System :: Networking
 Classifier: Topic :: System :: Systems Administration
 Classifier: Topic :: Utilities
-Requires-Python: >=3.8
+Requires-Python: >=3.9
 License-File: LICENSE.txt
-Requires-Dist: acme>=2.11.0
+Requires-Dist: acme>=4.0.0
 Requires-Dist: ConfigArgParse>=1.5.3
 Requires-Dist: configobj>=5.0.6
-Requires-Dist: cryptography>=3.2.1
+Requires-Dist: cryptography>=43.0.0
 Requires-Dist: distro>=1.0.1
-Requires-Dist: importlib_resources>=1.3.1; python_version < "3.9"
 Requires-Dist: importlib_metadata>=4.6; python_version < "3.10"
-Requires-Dist: josepy>=1.13.0
+Requires-Dist: josepy>=2.0.0
 Requires-Dist: parsedatetime>=2.4
 Requires-Dist: pyrfc3339
 Requires-Dist: pytz>=2019.3
 Requires-Dist: pywin32>=300; sys_platform == "win32"
-Requires-Dist: setuptools>=41.6.0
 Provides-Extra: all
 Requires-Dist: azure-devops; extra == "all"
 Requires-Dist: ipdb; extra == "all"
 Requires-Dist: poetry>=1.2.0; extra == "all"
-Requires-Dist: poetry-plugin-export>=1.1.0; extra == "all"
+Requires-Dist: poetry-plugin-export>=1.9.0; extra == "all"
 Requires-Dist: twine; extra == "all"
 Requires-Dist: Sphinx>=1.2; extra == "all"
 Requires-Dist: sphinx_rtd_theme; extra == "all"
@@ -58,19 +56,17 @@
 Requires-Dist: setuptools; extra == "all"
 Requires-Dist: tox; extra == "all"
 Requires-Dist: types-httplib2; extra == "all"
-Requires-Dist: types-pyOpenSSL; extra == "all"
 Requires-Dist: types-pyRFC3339; extra == "all"
 Requires-Dist: types-pytz; extra == "all"
 Requires-Dist: types-pywin32; extra == "all"
 Requires-Dist: types-requests; extra == "all"
 Requires-Dist: types-setuptools; extra == "all"
-Requires-Dist: types-six; extra == "all"
 Requires-Dist: wheel; extra == "all"
 Provides-Extra: dev
 Requires-Dist: azure-devops; extra == "dev"
 Requires-Dist: ipdb; extra == "dev"
 Requires-Dist: poetry>=1.2.0; extra == "dev"
-Requires-Dist: poetry-plugin-export>=1.1.0; extra == "dev"
+Requires-Dist: poetry-plugin-export>=1.9.0; extra == "dev"
 Requires-Dist: twine; extra == "dev"
 Provides-Extra: docs
 Requires-Dist: Sphinx>=1.2; extra == "docs"
@@ -86,24 +82,33 @@
 Requires-Dist: setuptools; extra == "test"
 Requires-Dist: tox; extra == "test"
 Requires-Dist: types-httplib2; extra == "test"
-Requires-Dist: types-pyOpenSSL; extra == "test"
 Requires-Dist: types-pyRFC3339; extra == "test"
 Requires-Dist: types-pytz; extra == "test"
 Requires-Dist: types-pywin32; extra == "test"
 Requires-Dist: types-requests; extra == "test"
 Requires-Dist: types-setuptools; extra == "test"
-Requires-Dist: types-six; extra == "test"
 Requires-Dist: wheel; extra == "test"
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: description
+Dynamic: home-page
+Dynamic: license
+Dynamic: license-file
+Dynamic: provides-extra
+Dynamic: requires-dist
+Dynamic: requires-python
+Dynamic: summary
 
 .. This file contains a series of comments that are used to include sections of this README in other files. Do not modify these comments unless you know what you are doing. tag:intro-begin
 
 |build-status|
 
-.. |build-status| image:: https://img.shields.io/azure-devops/build/certbot/ba534f81-a483-4b9b-9b4e-a60bec8fee72/5/master
+.. |build-status| image:: https://img.shields.io/azure-devops/build/certbot/ba534f81-a483-4b9b-9b4e-a60bec8fee72/5/main
    :target: https://dev.azure.com/certbot/certbot/_build?definitionId=5
    :alt: Azure Pipelines CI status
- 
-.. image:: https://raw.githubusercontent.com/EFForg/design/master/logos/eff-certbot-lockup.png
+
+.. image:: https://raw.githubusercontent.com/EFForg/design/master/logos/certbot/eff-certbot-lockup.png
   :width: 200
   :alt: EFF Certbot Logo
 
@@ -136,7 +141,7 @@
 
 Software project: https://github.com/certbot/certbot
 
-Changelog: https://github.com/certbot/certbot/blob/master/certbot/CHANGELOG.md
+Changelog: https://github.com/certbot/certbot/blob/main/certbot/CHANGELOG.md
 
 For Contributors: https://certbot.eff.org/docs/contributing.html
 
diff -Nru python-certbot-2.11.0/certbot.egg-info/requires.txt python-certbot-4.0.0/certbot.egg-info/requires.txt
--- python-certbot-2.11.0/certbot.egg-info/requires.txt	2024-06-05 17:34:03.000000000 -0400
+++ python-certbot-4.0.0/certbot.egg-info/requires.txt	2025-04-07 18:03:34.000000000 -0400
@@ -1,20 +1,16 @@
-acme>=2.11.0
+acme>=4.0.0
 ConfigArgParse>=1.5.3
 configobj>=5.0.6
-cryptography>=3.2.1
+cryptography>=43.0.0
 distro>=1.0.1
-josepy>=1.13.0
+josepy>=2.0.0
 parsedatetime>=2.4
 pyrfc3339
 pytz>=2019.3
-setuptools>=41.6.0
 
 [:python_version < "3.10"]
 importlib_metadata>=4.6
 
-[:python_version < "3.9"]
-importlib_resources>=1.3.1
-
 [:sys_platform == "win32"]
 pywin32>=300
 
@@ -22,7 +18,7 @@
 azure-devops
 ipdb
 poetry>=1.2.0
-poetry-plugin-export>=1.1.0
+poetry-plugin-export>=1.9.0
 twine
 Sphinx>=1.2
 sphinx_rtd_theme
@@ -36,20 +32,18 @@
 setuptools
 tox
 types-httplib2
-types-pyOpenSSL
 types-pyRFC3339
 types-pytz
 types-pywin32
 types-requests
 types-setuptools
-types-six
 wheel
 
 [dev]
 azure-devops
 ipdb
 poetry>=1.2.0
-poetry-plugin-export>=1.1.0
+poetry-plugin-export>=1.9.0
 twine
 
 [docs]
@@ -67,11 +61,9 @@
 setuptools
 tox
 types-httplib2
-types-pyOpenSSL
 types-pyRFC3339
 types-pytz
 types-pywin32
 types-requests
 types-setuptools
-types-six
 wheel
diff -Nru python-certbot-2.11.0/certbot.egg-info/SOURCES.txt python-certbot-4.0.0/certbot.egg-info/SOURCES.txt
--- python-certbot-2.11.0/certbot.egg-info/SOURCES.txt	2024-06-05 17:34:03.000000000 -0400
+++ python-certbot-4.0.0/certbot.egg-info/SOURCES.txt	2025-04-07 18:03:34.000000000 -0400
@@ -152,7 +152,6 @@
 certbot/tests/testdata/ocsp_responder_certificate.pem
 certbot/tests/testdata/os-release
 certbot/tests/testdata/rsa2048_key.pem
-certbot/tests/testdata/rsa256_key.pem
 certbot/tests/testdata/rsa512_key.pem
 certbot/tests/testdata/sample-renewal-ancient.conf
 certbot/tests/testdata/sample-renewal-deprecated-option.conf
diff -Nru python-certbot-2.11.0/CHANGELOG.md python-certbot-4.0.0/CHANGELOG.md
--- python-certbot-2.11.0/CHANGELOG.md	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/CHANGELOG.md	2025-04-07 18:03:33.000000000 -0400
@@ -2,6 +2,151 @@
 
 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
+
+*
+
+### Changed
+
+* 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._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`
+
+### Fixed
+
+* Fixed a bug introduced in Certbot 3.1.0 where OpenSSL environment variables
+  needed in our snap configuration were persisted in calls to external programs
+  like nginx which could cause them to fail to load OpenSSL.
+
+More details about these changes can be found on our GitHub repo.
+
+## 3.2.0 - 2025-02-11
+
+### Added
+
+*
+
+### Changed
+
+* certbot-nginx now requires pyparsing>=2.4.7.
+* certbot and its acme library now require cryptography>=43.0.0.
+* certbot-nginx and our acme library now require pyOpenSSL>=25.0.0.
+* Deprecated `gen_ss_cert` in `acme.crypto_util` as it uses deprecated
+  pyOpenSSL API.
+* Add `make_self_signed_cert` to `acme.crypto_util` to replace `gen_ss_cert.
+* Directory hooks are now run on all commands by default, not just `renew`
+* Help output now shows `False` as default when it can be set via `cli.ini` instead of `None`
+* Changed terms of service agreement text to have a newline after the TOS link
+* certbot-cloudflare-dns is now pinned to version 2.19 of Cloudflare's python library
+* Removed support for Linode API v3 which was sunset at the end of July 203.
+
+### Fixed
+
+* Private keys are now saved in PKCS#8 format instead of PKCS#1. Using PKCS#1
+  was a regression introduced in Certbot 3.1.0.
+* Allow nginx plugin to parse non-breaking spaces in nginx configuration files.
+* Honor --reuse-key when --allow-subset-of-names is set
+* Fixed regression in symlink parsing on Windows that was introduced in Certbot
+  3.1.0.
+* When adding ssl listen directives in nginx server blocks, IP addresses are now
+  preserved.
+* Nginx configurations can now have the http block in files other than the root (nginx.conf)
+* Nginx `server_name` directives with internal comments now ignore commented names
+
+More details about these changes can be found on our GitHub repo.
+
+## 3.1.0 - 2025-01-07
+
+### Added
+
+* Support for Python 3.13 was added.
+
+### Changed
+
+* Python 3.8 support was removed.
+* certbot-dns-rfc2136's minimum required version of dnspython is now 2.6.1.
+* Updated our Docker images to be based on Alpine Linux 3.20.
+* Our runtime dependency on setuptools has been dropped from all Certbot
+  components.
+* Certbot's packages no longer depend on library importlib_resources.
+
+### Fixed
+
+* Included an OpenSSL library that was missing in our Certbot snap fixing
+  crashes affecting 32-bit ARM users.
+
+More details about these changes can be found on our GitHub repo.
+
+## 3.0.1 - 2024-11-14
+
+### Fixed
+
+* Removed a CryptographyDeprecationWarning that was being displayed to users
+  when checking OCSP status.
+
+More details about these changes can be found on our GitHub repo.
+
+## 3.0.0 - 2024-11-05
+
+### Added
+
+*
+
+### Changed
+
+* The update_symlinks command was removed.
+* The `csr_dir` and `key_dir` attributes on
+  `certbot.configuration.NamespaceConfig` were removed.
+* The `--manual-public-ip-logging-ok` command line flag was removed.
+* The `--dns-route53-propagation-seconds` command line flag was removed.
+* The `certbot_dns_route53.authenticator` module has been removed. This should
+  not affect any users of the plugin and instead would only affect developers
+  trying to develop on top of the old code.
+* Support for Python 3.8 was deprecated and will be removed in our next planned
+  release.
+
+### Fixed
+
+*
+
+More details about these changes can be found on our GitHub repo.
+
 ## 2.11.0 - 2024-06-05
 
 ### Added
diff -Nru python-certbot-2.11.0/debian/changelog python-certbot-4.0.0/debian/changelog
--- python-certbot-2.11.0/debian/changelog	2025-04-30 09:05:24.000000000 -0400
+++ python-certbot-4.0.0/debian/changelog	2025-05-25 11:27:29.000000000 -0400
@@ -1,14 +1,17 @@
-python-certbot (2.11.0-1.1) unstable; urgency=medium
+python-certbot (4.0.0-2) unstable; urgency=medium
 
-  * Non-maintainer upload.
-  * Update debconf template translation
-     - Swedish translation.
-       Thanks to Martin Bagge / brother (Closes: #1072750)
-  * Added debconf template translation
-     - Catalan translation
-       Thanks to Carles Pina i Estany
+  * autopkgtests: drop manual IP flag no longer used
 
- -- Helge Kreutzmann <debian@helgefjell.de>  Wed, 30 Apr 2025 15:05:24 +0200
+ -- Harlan Lieberman-Berg <hlieberman@debian.org>  Sun, 25 May 2025 11:27:29 -0400
+
+python-certbot (4.0.0-1) unstable; urgency=medium
+
+  * Add Swedish po file (Closes: #1072750)
+  * New upstream version 4.0.0 (Closes: #1106462)
+  * Bump dependency requirements
+  * Refresh patches
+
+ -- Harlan Lieberman-Berg <hlieberman@debian.org>  Sat, 24 May 2025 17:04:42 -0400
 
 python-certbot (2.11.0-1) unstable; urgency=medium
 
@@ -46,7 +49,6 @@
 
   * Add certbot.7 (Closes: #1028535)
   * Add patch preserving keytype
-
  -- Harlan Lieberman-Berg <hlieberman@debian.org>  Tue, 28 Mar 2023 22:13:12 -0400
 
 python-certbot (2.1.0-2) unstable; urgency=medium
diff -Nru python-certbot-2.11.0/debian/control python-certbot-4.0.0/debian/control
--- python-certbot-2.11.0/debian/control	2025-04-30 09:05:24.000000000 -0400
+++ python-certbot-4.0.0/debian/control	2025-05-24 17:10:19.000000000 -0400
@@ -8,12 +8,12 @@
                dh-python,
                po-debconf,
                python3,
-               python3-acme-abi-2 (>= 2.11~),
+               python3-acme-abi-4 (>= 2.11~),
                python3-configargparse (>= 1.7~),
                python3-configobj,
-               python3-cryptography,
+               python3-cryptography (>= 43.0.0~),
                python3-distro,
-               python3-josepy (>= 1.13.0~),
+               python3-josepy (>= 2.0~),
                python3-parsedatetime,
                python3-pytest,
                python3-repoze.sphinx.autointerface,
@@ -31,7 +31,7 @@
 
 Package: python3-certbot
 Architecture: all
-Depends: python3-acme-abi-2 (>= ${Abi-major-minor-version}),
+Depends: python3-acme-abi-4 (>= ${Abi-major-minor-version}),
          python3-requests,
          ${misc:Depends},
          ${python3:Depends}
diff -Nru python-certbot-2.11.0/debian/patches/0001-remove-external-images.patch python-certbot-4.0.0/debian/patches/0001-remove-external-images.patch
--- python-certbot-2.11.0/debian/patches/0001-remove-external-images.patch	2025-04-30 09:05:24.000000000 -0400
+++ python-certbot-4.0.0/debian/patches/0001-remove-external-images.patch	2025-05-24 17:10:19.000000000 -0400
@@ -12,11 +12,11 @@
  
 -|build-status|
 -
--.. |build-status| image:: https://img.shields.io/azure-devops/build/certbot/ba534f81-a483-4b9b-9b4e-a60bec8fee72/5/master
+-.. |build-status| image:: https://img.shields.io/azure-devops/build/certbot/ba534f81-a483-4b9b-9b4e-a60bec8fee72/5/main
 -   :target: https://dev.azure.com/certbot/certbot/_build?definitionId=5
 -   :alt: Azure Pipelines CI status
-- 
--.. image:: https://raw.githubusercontent.com/EFForg/design/master/logos/eff-certbot-lockup.png
+-
+-.. image:: https://raw.githubusercontent.com/EFForg/design/master/logos/certbot/eff-certbot-lockup.png
 -  :width: 200
 -  :alt: EFF Certbot Logo
 -
diff -Nru python-certbot-2.11.0/debian/tests/http-01 python-certbot-4.0.0/debian/tests/http-01
--- python-certbot-2.11.0/debian/tests/http-01	2025-04-30 09:05:24.000000000 -0400
+++ python-certbot-4.0.0/debian/tests/http-01	2025-05-25 11:26:53.000000000 -0400
@@ -47,7 +47,6 @@
     --no-verify-ssl \
     --http-01-port 5002 \
     --https-port 5001 \
-    --manual-public-ip-logging-ok \
     --config-dir ${TMP_DIR}/certbot/http_01/conf \
     --work-dir ${TMP_DIR}/certbot/http_01/work \
     --logs-dir ${TMP_DIR}/certbot/http_01/logs \
diff -Nru python-certbot-2.11.0/docs/cli-help.txt python-certbot-4.0.0/docs/cli-help.txt
--- python-certbot-2.11.0/docs/cli-help.txt	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/docs/cli-help.txt	2025-04-07 18:03:33.000000000 -0400
@@ -36,7 +36,7 @@
   --agree-tos       Agree to the ACME server's Subscriber Agreement
    -m EMAIL         Email address for important account notifications
 
-options:
+optional arguments:
   -h, --help            show this help message and exit
   -c CONFIG_FILE, --config CONFIG_FILE
                         path to config file (default: /etc/letsencrypt/cli.ini
@@ -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/2.10.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,
@@ -208,14 +208,15 @@
                         enhance)
   --hsts                Add the Strict-Transport-Security header to every HTTP
                         response. Forcing browser to always use SSL for the
-                        domain. Defends against SSL Stripping. (default: None)
+                        domain. Defends against SSL Stripping. (default:
+                        False)
   --uir                 Add the "Content-Security-Policy: upgrade-insecure-
                         requests" header to every HTTP response. Forcing the
                         browser to use https:// for every http:// resource.
-                        (default: None)
+                        (default: False)
   --staple-ocsp         Enables OCSP Stapling. A valid OCSP response is
                         stapled to the certificate that the server offers
-                        during TLS. (default: None)
+                        during TLS. (default: False)
   --strict-permissions  Require that all configuration files are owned by the
                         current user; only needed if your config is somewhere
                         unsafe like /tmp/ (default: False)
@@ -362,7 +363,7 @@
                         shell constructs, so you can use this switch to
                         disable it. (default: False)
   --no-directory-hooks  Disable running executables found in Certbot's hook
-                        directories during renewal. (default: False)
+                        directories. (default: False)
   --disable-renew-updates
                         Disable automatic updates to your server configuration
                         that would otherwise be done by the selected installer
@@ -387,31 +388,23 @@
   --delete-after-revoke
                         Delete certificates after revoking them, along with
                         all previous and later versions of those certificates.
-                        (default: None)
+                        (default: Ask)
   --no-delete-after-revoke
                         Do not delete certificates after revoking them. This
                         option should be used with caution because the 'renew'
                         subcommand will attempt to renew undeleted revoked
-                        certificates. (default: None)
+                        certificates. (default: Ask)
 
 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:
                         u1@example.com,u2@example.com. (default: Ask).
-  --eff-email           Share your e-mail address with EFF (default: None)
+  --eff-email           Share your e-mail address with EFF (default: Ask)
   --no-eff-email        Don't share your e-mail address with EFF (default:
-                        None)
+                        Ask)
 
 update_account:
   Options for account modification
@@ -734,7 +727,7 @@
 
 webroot:
   Saves the necessary validation files to a .well-known/acme-challenge/
-  directory within the nominated webroot path. A seperate HTTP server must
+  directory within the nominated webroot path. A separate HTTP server must
   be running and serving files from the webroot path. HTTP challenge only
   (wildcards not supported).
 
diff -Nru python-certbot-2.11.0/docs/conf.py python-certbot-4.0.0/docs/conf.py
--- python-certbot-2.11.0/docs/conf.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/docs/conf.py	2025-04-07 18:03:33.000000000 -0400
@@ -314,5 +314,5 @@
 
 intersphinx_mapping = {
     'python': ('https://docs.python.org/', None),
-    'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
+    'acme': ('https://acme-python.readthedocs.io/en/latest/', None),
 }
diff -Nru python-certbot-2.11.0/docs/contributing.rst python-certbot-4.0.0/docs/contributing.rst
--- python-certbot-2.11.0/docs/contributing.rst	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/docs/contributing.rst	2025-04-07 18:03:33.000000000 -0400
@@ -17,8 +17,11 @@
 Windows, you'll need to set up a (virtual) machine running an OS such as Linux
 and continue with these instructions on that UNIX-like OS.
 
+If you're using macOS, it is recommended to first check out the `macOS
+suggestions`_ section before continuing with the installation instructions
+below.
+
 .. _local copy:
-.. _prerequisites:
 
 Running a local copy of the client
 ----------------------------------
@@ -47,13 +50,22 @@
    # NB2: RHEL-based distributions use python3X instead of python3 (e.g. python38)
    sudo dnf install python3 augeas-libs
    # For macOS installations with Homebrew already installed and configured
-   # NB: If you also run `brew install python` you don't need the ~/lib
-   #     directory created below, however, Certbot's Apache plugin won't work
-   #     if you use Python installed from other sources such as pyenv or the
-   #     version provided by Apple.
-   brew install augeas
+   # NB1: If you also run `brew install python` you don't need the ~/lib
+   #      directory created below, however, without this directory and symlinks
+   #      to augeas, Certbot's Apache plugin won't work if you use Python
+   #      installed from other sources such as pyenv or the version provided by
+   #      Apple.
+   # NB2: Some of our developer scripts expect GNU coreutils be first in your
+   #      PATH. The commands below set this up for bash and zsh, but your
+   #      instructions may be slightly different if you use an alternate shell.
+   brew install augeas coreutils gnu-sed
    mkdir ~/lib
-   ln -s $(brew --prefix)/lib/libaugeas* ~/lib
+   BREW_PREFIX=$(brew --prefix)
+   ln -s "$BREW_PREFIX"/lib/libaugeas* ~/lib
+   RC_LINE="export PATH=\"$BREW_PREFIX/opt/coreutils/libexec/gnubin:"
+   RC_LINE+="$BREW_PREFIX/opt/gnu-sed/libexec/gnubin:\$PATH\""
+   echo "$RC_LINE" >> ~/.bashrc  # for bash
+   echo "$RC_LINE" >> ~/.zshrc  # for zsh
 
 .. note:: If you have trouble creating the virtual environment below, you may
    need to install additional dependencies. See the `cryptography project's
@@ -66,7 +78,7 @@
 .. code-block:: shell
 
    cd certbot
-   python tools/venv.py
+   python3 tools/venv.py
 
 .. note:: You may need to repeat this when
   Certbot's dependencies change or when a new plugin is introduced.
@@ -92,17 +104,15 @@
 Find issues to work on
 ----------------------
 
-You can find the open issues in the `github issue tracker`_.  Comparatively
-easy ones are marked `good first issue`_.  If you're starting work on
-something, post a comment to let others know and seek feedback on your plan
-where appropriate.
+You can find the open issues in the `github issue tracker`_. If you're starting
+work on something, post a comment to let others know and seek feedback on your
+plan where appropriate.
 
 Once you've got a working branch, you can open a pull request.  All changes in
 your pull request must have thorough unit test coverage, pass our
 tests, and be compliant with the :ref:`coding style <coding-style>`.
 
 .. _github issue tracker: https://github.com/certbot/certbot/issues
-.. _good first issue: https://github.com/certbot/certbot/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22
 
 .. _testing:
 
@@ -115,33 +125,32 @@
 - running the `automated integration`_ tests
 - running an *ad hoc* `manual integration`_ test
 
-.. note:: Running integration tests does not currently work on macOS. See
-   https://github.com/certbot/certbot/issues/6959. In the meantime, we
-   recommend developers on macOS open a PR to run integration tests.
-
 .. _automated unit:
 
 Running automated unit tests
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-When you are working in a file ``foo.py``, there should also be a file ``foo_test.py``
-either in the same directory as ``foo.py`` or in the ``tests`` subdirectory
-(if there isn't, make one). While you are working on your code and tests, run
-``python foo_test.py`` to run the relevant tests.
+To run all unittests, mypy, and lint:
 
-For debugging, we recommend putting
-``import ipdb; ipdb.set_trace()`` statements inside the source code.
+.. code-block:: shell
+
+    tox
+
+If you're working on a specific test and would like to run just that one:
+
+.. code-block:: shell
 
-Once you are done with your code changes, and the tests in ``foo_test.py``
-pass, run all of the unit tests for Certbot and check for coverage with ``tox
--e cover``. You should then check for code style with ``tox run -e lint`` (all
-files) or ``pylint --rcfile=.pylintrc path/to/file.py`` (single file at a
-time).
-
-Once all of the above is successful, you may run the full test suite using
-``tox --skip-missing-interpreters``. We recommend running the commands above
-first, because running all tests like this is very slow, and the large amount
-of output can make it hard to find specific failures when they happen.
+    pytest acme/acme/_internal/tests/messages_test.py # Use the test file you're working on
+
+To run a specific test case within a file:
+
+.. code-block:: shell
+
+    pytest acme/acme/_internal/tests/messages_test.py -k test_to_partial_json
+
+For debugging, we recommend putting
+``import ipdb; ipdb.set_trace()`` statements inside the source code, which will require
+adding the `-s` flag to `pytest` invocations.
 
 .. warning:: The full test suite may attempt to modify your system's Apache
   config if your user has sudo permissions, so it should not be run on a
@@ -183,7 +192,7 @@
 - Docker installed, and a user with access to the Docker client,
 - an available `local copy`_ of Certbot.
 
-The virtual environment set up with `python tools/venv.py` contains two CLI tools
+The virtual environment set up with `python3 tools/venv.py` contains two CLI tools
 that can be used once the virtual environment is activated:
 
 .. code-block:: shell
@@ -211,7 +220,7 @@
 
 .. code-block:: shell
 
-    python tools/venv.py
+    python3 tools/venv.py
     source venv/bin/activate
     run_acme_server &
     certbot_test certonly --standalone -d test.example.com
@@ -236,8 +245,6 @@
   client code to configure specific web servers
 certbot-dns-*
   client code to configure DNS providers
-windows installer
-  Installs Certbot on Windows and is built using the files in windows-installer/
 
 Plugin-architecture
 -------------------
@@ -254,8 +261,8 @@
 plugins, implement both interfaces and perform both tasks. Others, like the
 built-in Standalone authenticator, implement just one interface.
 
-.. _interfaces.py: https://github.com/certbot/certbot/blob/master/certbot/certbot/interfaces.py
-.. _plugins/common.py: https://github.com/certbot/certbot/blob/master/certbot/certbot/plugins/common.py#L45
+.. _interfaces.py: https://github.com/certbot/certbot/blob/main/certbot/certbot/interfaces.py
+.. _plugins/common.py: https://github.com/certbot/certbot/blob/main/certbot/certbot/plugins/common.py#L45
 
 
 Authenticators
@@ -375,8 +382,8 @@
 `snap content interface`_ where ``certbot-1`` is the value for the ``content``
 attribute. The Certbot snap only uses this to find the names of connected
 plugin snaps and it expects to find the Python modules to be loaded under
-``lib/python3.8/site-packages/`` in the plugin snap. This location is the
-default when using the ``core20`` `base snap`_ and the `python snapcraft
+``lib/python3.12/site-packages/`` in the plugin snap. This location is the
+default when using the ``core24`` `base snap`_ and the `python snapcraft
 plugin`_.
 
 The Certbot snap also provides a separate content interface which
@@ -385,7 +392,7 @@
 
 The script used to generate the snapcraft.yaml files for our own externally
 snapped plugins can be found at
-https://github.com/certbot/certbot/blob/master/tools/snap/generate_dnsplugins_snapcraft.sh.
+https://github.com/certbot/certbot/blob/main/tools/snap/generate_dnsplugins_snapcraft.sh.
 
 For more information on building externally snapped plugins, see the section on
 :ref:`Building snaps`.
@@ -558,7 +565,7 @@
 
 Instructions for how to manually build and run the Certbot snap and the externally
 snapped DNS plugins that the Certbot project supplies are located in the README
-file at https://github.com/certbot/certbot/tree/master/tools/snap.
+file at https://github.com/certbot/certbot/tree/main/tools/snap.
 
 Updating the documentation
 ==========================
@@ -585,8 +592,7 @@
 
 We attempt to pin all of Certbot's dependencies whenever we can for reliability
 and consistency. Some of the places we have Certbot's dependencies pinned
-include our snaps, Docker images, Windows installer, CI, and our development
-environments.
+include our snaps, Docker images, CI, and our development environments.
 
 In most cases, the file where dependency versions are specified is
 ``tools/requirements.txt``. The one exception to this is our "oldest" tests
@@ -625,25 +631,42 @@
 Choosing dependency versions
 ----------------------------
 
-A number of Unix distributions create third-party Certbot packages for their users.
-Where feasible, the Certbot project tries to manage its dependencies in a way that
-does not create avoidable work for packagers.
-
-Avoiding adding new dependencies is a good way to help with this.
-
-When adding new or upgrading existing Python dependencies, Certbot developers should
-pay attention to which distributions are actively packaging Certbot. In particular:
-
-- EPEL (used by RHEL/CentOS/Fedora) updates Certbot regularly. At the time of writing,
-  EPEL9 is the release of EPEL where Certbot is being updated, but check the `EPEL
-  home page <https://docs.fedoraproject.org/en-US/epel/>`_ and `pkgs.org
-  <https://pkgs.org/search/?q=python3-certbot>`_ for the latest release.
-- Debian and Ubuntu only package Certbot when making new releases of their distros.
-  Checking the available version of dependencies in Debian "sid" and "unstable" can help
-  to identify dependencies that are likely to be available in the next stable release of
-  these distros.
-
-If a dependency is already packaged in these distros and is acceptable for use in Certbot,
-the oldest packaged version of that dependency should be chosen and set as the minimum
-version in ``setup.py``.
+When choosing dependency versions, we should choose whatever minimum versions
+simplify development of Certbot and our own distribution methods such as snaps,
+pip, and docker. Since these approaches have full access to PyPI, it's OK if
+the required packages declared in ``setup.py`` are quite new.
+
+If this approach to development creates significant trouble for some of our users, we
+can revisit this decision and weigh their trouble against the difficulties
+involved in maintaining support for a wider range of package versions. When
+doing this, we should also be sure to consider the feasibility of users getting
+access to these newer packages on their system rather than changing our own
+approach here. Their OS distribution may be able to package it, especially in
+an alternate repository and/or for a different version of Python to help avoid
+conflicts with other packages on their system.
+
+macOS suggestions
+=================
+
+If you're developing on macOS, before :ref:`setting up your Certbot development
+environment <local copy>`, it is recommended you perform the following steps.
+None of this is required, but it is the approach used by all/most of the
+current Certbot developers on macOS as of writing this:
+
+0. Install `Homebrew <https://brew.sh/>`_. It is the most popular package
+   manager on macOS by a wide margin and works well enough.
+1. Install `pyenv <https://github.com/pyenv/pyenv>`_, ideally through Homebrew
+   by running ``brew install pyenv``. Using Homebrew's Python for Certbot
+   development is annoying because it regularly updates and every time it does
+   it breaks your virtual environments. Using Python from ``pyenv`` avoids this
+   problem and gives you easy access to all versions of Python.
+2. If you're using ``pyenv``, make sure you've set up your shell for it by
+   following instructions like
+   https://github.com/pyenv/pyenv?tab=readme-ov-file#set-up-your-shell-environment-for-pyenv.
+3. Configure ``git`` to ignore the ``.DS_Store`` files that are created by
+   macOS's file manager Finder by running something like:
+
+.. code-block:: shell
 
+   mkdir -p ~/.config/git
+   echo '.DS_Store' >> ~/.config/git/ignore
diff -Nru python-certbot-2.11.0/docs/packaging.rst python-certbot-4.0.0/docs/packaging.rst
--- python-certbot-2.11.0/docs/packaging.rst	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/docs/packaging.rst	2025-04-07 18:03:33.000000000 -0400
@@ -25,7 +25,7 @@
 
 The following scripts are used in the process:
 
-- https://github.com/certbot/certbot/blob/master/tools/release.sh
+- https://github.com/certbot/certbot/blob/main/tools/release.sh
 
 We use git tags to identify releases, using `Semantic Versioning`_. For
 example: `v0.11.1`.
@@ -50,7 +50,7 @@
 Notes for package maintainers
 =============================
 
-0. Please use our tagged releases, not ``master``!
+0. Please use our tagged releases, not ``main``!
 
 1. Do not package ``certbot-compatibility-test`` as it's only used internally.
 
diff -Nru python-certbot-2.11.0/docs/using.rst python-certbot-4.0.0/docs/using.rst
--- python-certbot-2.11.0/docs/using.rst	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/docs/using.rst	2025-04-07 18:03:33.000000000 -0400
@@ -99,7 +99,7 @@
 ------
 
 The Apache plugin currently `supports
-<https://github.com/certbot/certbot/blob/master/certbot-apache/certbot_apache/_internal/entrypoint.py>`_
+<https://github.com/certbot/certbot/blob/main/certbot-apache/certbot_apache/_internal/entrypoint.py>`_
 modern OSes based on Debian, Fedora, SUSE, Gentoo, CentOS and Darwin.
 This automates both obtaining *and* installing certificates on an Apache
 webserver. To specify this plugin on the command line, simply include
@@ -298,9 +298,9 @@
 other developers. Many are beta/experimental, but some are already in
 widespread use:
 
-================== ==== ==== ===============================================================
+================== ==== ==== =================================================================
 Plugin             Auth Inst Notes
-================== ==== ==== ===============================================================
+================== ==== ==== =================================================================
 haproxy_           Y    Y    Integration with the HAProxy load balancer
 s3front_           Y    Y    Integration with Amazon CloudFront distribution of S3 buckets
 gandi_             Y    N    Obtain certificates via the Gandi LiveDNS API
@@ -310,6 +310,7 @@
 proxmox_           N    Y    Install certificates in Proxmox Virtualization servers
 dns-standalone_    Y    N    Obtain certificates via an integrated DNS server
 dns-ispconfig_     Y    N    DNS Authentication using ISPConfig as DNS server
+dns-cloudns_       Y    N    DNS Authentication using ClouDNS API
 dns-clouddns_      Y    N    DNS Authentication using CloudDNS API
 dns-lightsail_     Y    N    DNS Authentication using Amazon Lightsail DNS API
 dns-inwx_          Y    Y    DNS Authentication for INWX through the XML API
@@ -327,7 +328,9 @@
 dns-solidserver_   Y    N    DNS Authentication using SOLIDserver (EfficientIP)
 dns-stackit_       Y    N    DNS Authentication using STACKIT DNS
 dns-ionos_         Y    N    DNS Authentication using IONOS Cloud DNS
-================== ==== ==== ===============================================================
+dns-mijn-host_     Y    N    DNS Authentication using mijn.host DNS
+nginx-unit_        Y    Y    Automates obtaining and installing a certificate with Nginx Unit
+================== ==== ==== =================================================================
 
 .. _haproxy: https://github.com/greenhost/certbot-haproxy
 .. _s3front: https://github.com/dlapiduz/letsencrypt-s3front
@@ -338,6 +341,7 @@
 .. _external-auth: https://github.com/EnigmaBridge/certbot-external-auth
 .. _dns-standalone: https://github.com/siilike/certbot-dns-standalone
 .. _dns-ispconfig: https://github.com/m42e/certbot-dns-ispconfig
+.. _dns-cloudns: https://github.com/inventage/certbot-dns-cloudns
 .. _dns-clouddns: https://github.com/vshosting/certbot-dns-clouddns
 .. _dns-lightsail: https://github.com/noi/certbot-dns-lightsail
 .. _dns-inwx: https://github.com/oGGy990/certbot-dns-inwx/
@@ -355,6 +359,8 @@
 .. _dns-solidserver: https://gitlab.com/charlyhong/certbot-dns-solidserver
 .. _dns-stackit: https://github.com/stackitcloud/certbot-dns-stackit
 .. _dns-ionos: https://github.com/ionos-cloud/certbot-dns-ionos-cloud
+.. _dns-mijn-host: https://github.com/mijnhost/certbot-dns-mijn-host
+.. _nginx-unit: https://github.com/kea/certbot-nginx-unit
 
 If you're interested, you can also :ref:`write your own plugin <dev-plugin>`.
 
@@ -622,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.
 
@@ -633,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
@@ -679,10 +685,13 @@
 ``/etc/letsencrypt/renewal-hooks/pre``,
 ``/etc/letsencrypt/renewal-hooks/deploy``, and
 ``/etc/letsencrypt/renewal-hooks/post`` will be run as pre, deploy, and post
-hooks respectively when any certificate is renewed with the ``renew``
-subcommand. These hooks are run in alphabetical order and are not run for other
-subcommands. (The order the hooks are run is determined by the byte value of
-the characters in their filenames and is not dependent on your locale.)
+hooks respectively. These hooks are run in alphabetical order. (The order the
+hooks are run is determined by the byte value of the characters in their
+filenames and is not dependent on your locale.)
+
+Prior to certbot 3.2.0, hooks in directories were only run when certificates
+were renewed with the ``renew`` subcommand, but as of 3.2.0, they are run for
+any subcommand.
 
 Hooks specified in the command line, :ref:`configuration file
 <config-file>`, or :ref:`renewal configuration files <renewal-config-file>` are
diff -Nru python-certbot-2.11.0/examples/cli.ini python-certbot-4.0.0/examples/cli.ini
--- python-certbot-2.11.0/examples/cli.ini	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/examples/cli.ini	2025-04-07 18:03:33.000000000 -0400
@@ -17,7 +17,7 @@
 # Uncomment and update to register with the specified e-mail address
 # email = foo@example.com
 
-# Uncomment to use the standalone authenticator on port 443
+# Uncomment to use the standalone authenticator on port 80
 # authenticator = standalone
 
 # Uncomment to use the webroot authenticator. Replace webroot-path with the
diff -Nru python-certbot-2.11.0/PKG-INFO python-certbot-4.0.0/PKG-INFO
--- python-certbot-2.11.0/PKG-INFO	2024-06-05 17:34:03.529808500 -0400
+++ python-certbot-4.0.0/PKG-INFO	2025-04-07 18:03:34.270558800 -0400
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: certbot
-Version: 2.11.0
+Version: 4.0.0
 Summary: ACME client
 Home-page: https://github.com/certbot/certbot
 Author: Certbot Project
@@ -14,37 +14,35 @@
 Classifier: Operating System :: POSIX :: Linux
 Classifier: Programming Language :: Python
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
 Classifier: Topic :: Internet :: WWW/HTTP
 Classifier: Topic :: Security
 Classifier: Topic :: System :: Installation/Setup
 Classifier: Topic :: System :: Networking
 Classifier: Topic :: System :: Systems Administration
 Classifier: Topic :: Utilities
-Requires-Python: >=3.8
+Requires-Python: >=3.9
 License-File: LICENSE.txt
-Requires-Dist: acme>=2.11.0
+Requires-Dist: acme>=4.0.0
 Requires-Dist: ConfigArgParse>=1.5.3
 Requires-Dist: configobj>=5.0.6
-Requires-Dist: cryptography>=3.2.1
+Requires-Dist: cryptography>=43.0.0
 Requires-Dist: distro>=1.0.1
-Requires-Dist: importlib_resources>=1.3.1; python_version < "3.9"
 Requires-Dist: importlib_metadata>=4.6; python_version < "3.10"
-Requires-Dist: josepy>=1.13.0
+Requires-Dist: josepy>=2.0.0
 Requires-Dist: parsedatetime>=2.4
 Requires-Dist: pyrfc3339
 Requires-Dist: pytz>=2019.3
 Requires-Dist: pywin32>=300; sys_platform == "win32"
-Requires-Dist: setuptools>=41.6.0
 Provides-Extra: all
 Requires-Dist: azure-devops; extra == "all"
 Requires-Dist: ipdb; extra == "all"
 Requires-Dist: poetry>=1.2.0; extra == "all"
-Requires-Dist: poetry-plugin-export>=1.1.0; extra == "all"
+Requires-Dist: poetry-plugin-export>=1.9.0; extra == "all"
 Requires-Dist: twine; extra == "all"
 Requires-Dist: Sphinx>=1.2; extra == "all"
 Requires-Dist: sphinx_rtd_theme; extra == "all"
@@ -58,19 +56,17 @@
 Requires-Dist: setuptools; extra == "all"
 Requires-Dist: tox; extra == "all"
 Requires-Dist: types-httplib2; extra == "all"
-Requires-Dist: types-pyOpenSSL; extra == "all"
 Requires-Dist: types-pyRFC3339; extra == "all"
 Requires-Dist: types-pytz; extra == "all"
 Requires-Dist: types-pywin32; extra == "all"
 Requires-Dist: types-requests; extra == "all"
 Requires-Dist: types-setuptools; extra == "all"
-Requires-Dist: types-six; extra == "all"
 Requires-Dist: wheel; extra == "all"
 Provides-Extra: dev
 Requires-Dist: azure-devops; extra == "dev"
 Requires-Dist: ipdb; extra == "dev"
 Requires-Dist: poetry>=1.2.0; extra == "dev"
-Requires-Dist: poetry-plugin-export>=1.1.0; extra == "dev"
+Requires-Dist: poetry-plugin-export>=1.9.0; extra == "dev"
 Requires-Dist: twine; extra == "dev"
 Provides-Extra: docs
 Requires-Dist: Sphinx>=1.2; extra == "docs"
@@ -86,24 +82,33 @@
 Requires-Dist: setuptools; extra == "test"
 Requires-Dist: tox; extra == "test"
 Requires-Dist: types-httplib2; extra == "test"
-Requires-Dist: types-pyOpenSSL; extra == "test"
 Requires-Dist: types-pyRFC3339; extra == "test"
 Requires-Dist: types-pytz; extra == "test"
 Requires-Dist: types-pywin32; extra == "test"
 Requires-Dist: types-requests; extra == "test"
 Requires-Dist: types-setuptools; extra == "test"
-Requires-Dist: types-six; extra == "test"
 Requires-Dist: wheel; extra == "test"
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: description
+Dynamic: home-page
+Dynamic: license
+Dynamic: license-file
+Dynamic: provides-extra
+Dynamic: requires-dist
+Dynamic: requires-python
+Dynamic: summary
 
 .. This file contains a series of comments that are used to include sections of this README in other files. Do not modify these comments unless you know what you are doing. tag:intro-begin
 
 |build-status|
 
-.. |build-status| image:: https://img.shields.io/azure-devops/build/certbot/ba534f81-a483-4b9b-9b4e-a60bec8fee72/5/master
+.. |build-status| image:: https://img.shields.io/azure-devops/build/certbot/ba534f81-a483-4b9b-9b4e-a60bec8fee72/5/main
    :target: https://dev.azure.com/certbot/certbot/_build?definitionId=5
    :alt: Azure Pipelines CI status
- 
-.. image:: https://raw.githubusercontent.com/EFForg/design/master/logos/eff-certbot-lockup.png
+
+.. image:: https://raw.githubusercontent.com/EFForg/design/master/logos/certbot/eff-certbot-lockup.png
   :width: 200
   :alt: EFF Certbot Logo
 
@@ -136,7 +141,7 @@
 
 Software project: https://github.com/certbot/certbot
 
-Changelog: https://github.com/certbot/certbot/blob/master/certbot/CHANGELOG.md
+Changelog: https://github.com/certbot/certbot/blob/main/certbot/CHANGELOG.md
 
 For Contributors: https://certbot.eff.org/docs/contributing.html
 
diff -Nru python-certbot-2.11.0/README.rst python-certbot-4.0.0/README.rst
--- python-certbot-2.11.0/README.rst	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/README.rst	2025-04-07 18:03:33.000000000 -0400
@@ -2,11 +2,11 @@
 
 |build-status|
 
-.. |build-status| image:: https://img.shields.io/azure-devops/build/certbot/ba534f81-a483-4b9b-9b4e-a60bec8fee72/5/master
+.. |build-status| image:: https://img.shields.io/azure-devops/build/certbot/ba534f81-a483-4b9b-9b4e-a60bec8fee72/5/main
    :target: https://dev.azure.com/certbot/certbot/_build?definitionId=5
    :alt: Azure Pipelines CI status
- 
-.. image:: https://raw.githubusercontent.com/EFForg/design/master/logos/eff-certbot-lockup.png
+
+.. image:: https://raw.githubusercontent.com/EFForg/design/master/logos/certbot/eff-certbot-lockup.png
   :width: 200
   :alt: EFF Certbot Logo
 
@@ -39,7 +39,7 @@
 
 Software project: https://github.com/certbot/certbot
 
-Changelog: https://github.com/certbot/certbot/blob/master/certbot/CHANGELOG.md
+Changelog: https://github.com/certbot/certbot/blob/main/certbot/CHANGELOG.md
 
 For Contributors: https://certbot.eff.org/docs/contributing.html
 
diff -Nru python-certbot-2.11.0/setup.py python-certbot-4.0.0/setup.py
--- python-certbot-2.11.0/setup.py	2024-06-05 17:34:02.000000000 -0400
+++ python-certbot-4.0.0/setup.py	2025-04-07 18:03:33.000000000 -0400
@@ -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:
@@ -30,18 +31,16 @@
     f'acme>={version}',
     'ConfigArgParse>=1.5.3',
     'configobj>=5.0.6',
-    'cryptography>=3.2.1',
+    'cryptography>=43.0.0',
     'distro>=1.0.1',
-    'importlib_resources>=1.3.1; python_version < "3.9"',
     'importlib_metadata>=4.6; python_version < "3.10"',
-    'josepy>=1.13.0',
+    'josepy>=2.0.0',
     'parsedatetime>=2.4',
     'pyrfc3339',
     'pytz>=2019.3',
     # This dependency needs to be added using environment markers to avoid its
     # installation on Linux.
     'pywin32>=300 ; sys_platform == "win32"',
-    'setuptools>=41.6.0',
 ]
 
 dev_extras = [
@@ -50,10 +49,8 @@
     # poetry 1.2.0+ is required for it to pin pip, setuptools, and wheel. See
     # https://github.com/python-poetry/poetry/issues/1584.
     'poetry>=1.2.0',
-    # poetry-plugin-export>=1.1.0 is required to use the constraints.txt export
-    # format. See
-    # https://github.com/python-poetry/poetry-plugin-export/blob/efcfd34859e72f6a79a80398f197ce6eb2bbd7cd/CHANGELOG.md#added.
-    'poetry-plugin-export>=1.1.0',
+    # allows us to use newer urllib3 https://github.com/python-poetry/poetry-plugin-export/issues/183
+    'poetry-plugin-export>=1.9.0',
     'twine',
 ]
 
@@ -77,13 +74,11 @@
     'setuptools',
     'tox',
     'types-httplib2',
-    'types-pyOpenSSL',
     'types-pyRFC3339',
     'types-pytz',
     'types-pywin32',
     'types-requests',
     'types-setuptools',
-    'types-six',
     'wheel',
 ]
 
@@ -99,7 +94,7 @@
     author="Certbot Project",
     author_email='certbot-dev@eff.org',
     license='Apache License 2.0',
-    python_requires='>=3.8',
+    python_requires='>=3.9',
     classifiers=[
         'Development Status :: 5 - Production/Stable',
         'Environment :: Console',
@@ -109,11 +104,11 @@
         'Operating System :: POSIX :: Linux',
         'Programming Language :: Python',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',
         'Programming Language :: Python :: 3.10',
         'Programming Language :: Python :: 3.11',
         'Programming Language :: Python :: 3.12',
+        'Programming Language :: Python :: 3.13',
         'Topic :: Internet :: WWW/HTTP',
         'Topic :: Security',
         'Topic :: System :: Installation/Setup',

Reply to: