Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-certbot for openSUSE:Factory checked in at 2026-03-16 15:49:10 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-certbot (Old) and /work/SRC/openSUSE:Factory/.python-certbot.new.8177 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-certbot" Mon Mar 16 15:49:10 2026 rev:65 rq:1339343 version:5.4.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-certbot/python-certbot.changes 2026-02-24 18:31:22.936203306 +0100 +++ /work/SRC/openSUSE:Factory/.python-certbot.new.8177/python-certbot.changes 2026-03-16 15:49:16.334153905 +0100 @@ -1,0 +2,7 @@ +Mon Mar 16 12:12:03 UTC 2026 - Markéta Machová <[email protected]> + +- Update to 5.4.0 + * The webroot plugin now supports IP address issuance. +- Drop merged patch reset-mock-call-count.patch + +------------------------------------------------------------------- Old: ---- certbot-5.3.1.tar.gz reset-mock-call-count.patch New: ---- certbot-5.4.0.tar.gz ----------(Old B)---------- Old: * The webroot plugin now supports IP address issuance. - Drop merged patch reset-mock-call-count.patch ----------(Old E)---------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-certbot.spec ++++++ --- /var/tmp/diff_new_pack.CBKyJZ/_old 2026-03-16 15:49:17.174188659 +0100 +++ /var/tmp/diff_new_pack.CBKyJZ/_new 2026-03-16 15:49:17.178188824 +0100 @@ -23,14 +23,12 @@ %endif %{?sle15_python_module_pythons} Name: python-certbot -Version: 5.3.1 +Version: 5.4.0 Release: 0 Summary: ACME client License: Apache-2.0 URL: https://github.com/certbot/certbot Source0: https://files.pythonhosted.org/packages/source/c/certbot/certbot-%{version}.tar.gz -# https://github.com/certbot/certbot/pull/10576 Reset mock call count using reset_mock since new thread-safe implementation means it can no longer just be set to 0 -Patch0: reset-mock-call-count.patch BuildRequires: %{python_module acme >= %{version}} BuildRequires: %{python_module configargparse >= 1.5.3} BuildRequires: %{python_module configobj >= 5.0.6} ++++++ certbot-5.3.1.tar.gz -> certbot-5.4.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-5.3.1/CHANGELOG.md new/certbot-5.4.0/CHANGELOG.md --- old/certbot-5.3.1/CHANGELOG.md 2026-02-09 22:19:25.000000000 +0100 +++ new/certbot-5.4.0/CHANGELOG.md 2026-03-10 18:46:56.000000000 +0100 @@ -4,6 +4,17 @@ <!-- towncrier release notes start --> +## 5.4.0 - 2026-03-10 + +### Added + +- The webroot plugin now supports IP address issuance. ([#10543](https://github.com/certbot/certbot/issues/10543)) + +### Changed + +- certbot-nginx now requires pyparsing>=3.0.0. ([#10560](https://github.com/certbot/certbot/issues/10560)) + + ## 5.3.1 - 2026-02-09 ### Fixed @@ -15,7 +26,7 @@ ### Added -- A new command line flag, --ip-address, has been added. This requests certificates with IP address SANs when using the standalone or manual plugin. Note that for Let's Encrypt's implementation of IP address certificates, you'll also need to pass `--preferred-profile shortlived`. ([#10465](https://github.com/certbot/certbot/issues/10465)) +- A new command line flag, --ip-address, has been added. This requests certificates with IP address SANs when using the standalone or manual plugin. Note that for Let's Encrypt's implementation of IP address certificates, you'll also need to pass `--preferred-profile shortlived`. ([#10495](https://github.com/certbot/certbot/issues/10495), [#10544](https://github.com/certbot/certbot/pull/10544)) ### Changed diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-5.3.1/PKG-INFO new/certbot-5.4.0/PKG-INFO --- old/certbot-5.3.1/PKG-INFO 2026-02-09 22:19:26.456840500 +0100 +++ new/certbot-5.4.0/PKG-INFO 2026-03-10 18:46:57.393162700 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: certbot -Version: 5.3.1 +Version: 5.4.0 Summary: ACME client Author: Certbot Project License-Expression: Apache-2.0 @@ -25,7 +25,7 @@ Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE.txt -Requires-Dist: acme>=5.3.1 +Requires-Dist: acme>=5.4.0 Requires-Dist: ConfigArgParse>=1.5.3 Requires-Dist: configobj>=5.0.6 Requires-Dist: cryptography>=43.0.0 @@ -156,3 +156,10 @@ * Configuration changes are logged and can be reverted. .. Do not modify this comment unless you know what you're doing. tag:features-end + +Thanks +------ + +We appreciate `Digital Ocean`_ for donating credits to help us test and develop Certbot. + +.. _Digital Ocean: https://www.digitalocean.com/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-5.3.1/README.rst new/certbot-5.4.0/README.rst --- old/certbot-5.3.1/README.rst 2026-02-09 22:19:24.000000000 +0100 +++ new/certbot-5.4.0/README.rst 2026-03-10 18:46:56.000000000 +0100 @@ -85,3 +85,10 @@ * Configuration changes are logged and can be reverted. .. Do not modify this comment unless you know what you're doing. tag:features-end + +Thanks +------ + +We appreciate `Digital Ocean`_ for donating credits to help us test and develop Certbot. + +.. _Digital Ocean: https://www.digitalocean.com/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-5.3.1/docs/cli-help.txt new/certbot-5.4.0/docs/cli-help.txt --- old/certbot-5.3.1/docs/cli-help.txt 2026-02-09 22:19:24.000000000 +0100 +++ new/certbot-5.4.0/docs/cli-help.txt 2026-03-10 18:46:56.000000000 +0100 @@ -38,7 +38,7 @@ options: -h, --help show this help message and exit - -c, --config CONFIG_FILE + -c CONFIG_FILE, --config CONFIG_FILE path to config file (default: /etc/letsencrypt/cli.ini and ~/.config/letsencrypt/cli.ini) -v, --verbose This flag can be used multiple times to incrementally @@ -58,7 +58,7 @@ --force-interactive Force Certbot to be interactive even if it detects it's not being run in a terminal. This flag cannot be used with the renew subcommand. (default: False) - -d, --domains, --domain DOMAIN + -d DOMAIN, --domains DOMAIN, --domain DOMAIN Domain names to include. For multiple domains you can use multiple -d flags or enter a comma separated list of domains as a parameter. All domains will be @@ -147,7 +147,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/5.3.0 (certbot; + "". (default: CertbotACMEClient/5.3.1 (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, @@ -424,7 +424,8 @@ register: Options for account registration - -m, --email EMAIL Email used for registration and recovery contact. Use + -m EMAIL, --email EMAIL + Email used for registration and recovery contact. Use comma to register multiple emails, ex: [email protected],[email protected]. (default: Ask). --eff-email Share your e-mail address with EFF (default: Ask) @@ -477,9 +478,9 @@ Name of the plugin that is both an authenticator and an installer. Should not be used together with --authenticator or --installer. (default: Ask) - -a, --authenticator AUTHENTICATOR + -a AUTHENTICATOR, --authenticator AUTHENTICATOR Authenticator plugin name. (default: None) - -i, --installer INSTALLER + -i INSTALLER, --installer INSTALLER Installer plugin name (also used to find domains). (default: None) --apache Obtain and install certificates using Apache (default: @@ -756,7 +757,7 @@ be running and serving files from the webroot path. HTTP challenge only (wildcards not supported). - --webroot-path, -w WEBROOT_PATH + --webroot-path WEBROOT_PATH, -w WEBROOT_PATH public_html / webroot path. This can be specified multiple times to handle different domains; each domain will have the webroot path that preceded it. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-5.3.1/docs/using.rst new/certbot-5.4.0/docs/using.rst --- old/certbot-5.3.1/docs/using.rst 2026-02-09 22:19:24.000000000 +0100 +++ new/certbot-5.4.0/docs/using.rst 2026-03-10 18:46:56.000000000 +0100 @@ -350,7 +350,7 @@ .. _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/ -.. _dns-azure: https://github.com/binkhq/certbot-dns-azure +.. _dns-azure: https://github.com/terricain/certbot-dns-azure .. _dns-godaddy: https://github.com/miigotu/certbot-dns-godaddy .. _dns-yandexcloud: https://github.com/PykupeJIbc/certbot-dns-yandexcloud .. _dns-bunny: https://github.com/mwt/certbot-dns-bunny diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-5.3.1/src/certbot/__init__.py new/certbot-5.4.0/src/certbot/__init__.py --- old/certbot-5.3.1/src/certbot/__init__.py 2026-02-09 22:19:25.000000000 +0100 +++ new/certbot-5.4.0/src/certbot/__init__.py 2026-03-10 18:46:56.000000000 +0100 @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '5.3.1' +__version__ = '5.4.0' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-5.3.1/src/certbot/_internal/cli/__init__.py new/certbot-5.4.0/src/certbot/_internal/cli/__init__.py --- old/certbot-5.3.1/src/certbot/_internal/cli/__init__.py 2026-02-09 22:19:24.000000000 +0100 +++ new/certbot-5.4.0/src/certbot/_internal/cli/__init__.py 2026-03-10 18:46:56.000000000 +0100 @@ -25,12 +25,13 @@ from certbot._internal.cli.cli_utils import _EncodeReasonAction from certbot._internal.cli.cli_utils import _PrefChallAction from certbot._internal.cli.cli_utils import _user_agent_comment_type -from certbot._internal.cli.cli_utils import add_domains +from certbot._internal.cli.cli_utils import add_dns_name +from certbot._internal.cli.cli_utils import add_ip_address from certbot._internal.cli.cli_utils import CaseInsensitiveList from certbot._internal.cli.cli_utils import config_help from certbot._internal.cli.cli_utils import CustomHelpFormatter from certbot._internal.cli.cli_utils import DomainsAction -from certbot._internal.cli.cli_utils import _IPAddressAction +from certbot._internal.cli.cli_utils import IPAddressAction from certbot._internal.cli.cli_utils import flag_default from certbot._internal.cli.cli_utils import HelpfulArgumentGroup from certbot._internal.cli.cli_utils import nonnegative_int @@ -125,7 +126,7 @@ helpful.add( [None, "certonly", "certificates"], "--ip-address", dest="ip_addresses", - action=_IPAddressAction, + action=IPAddressAction, default=flag_default("ip_addresses"), help="IP addresses to include. For multiple IP addresses you can use multiple " "--ip-address flags. All IP addresses will be included as Subject Alternative Names " diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-5.3.1/src/certbot/_internal/cli/cli_utils.py new/certbot-5.4.0/src/certbot/_internal/cli/cli_utils.py --- old/certbot-5.3.1/src/certbot/_internal/cli/cli_utils.py 2026-02-09 22:19:24.000000000 +0100 +++ new/certbot-5.4.0/src/certbot/_internal/cli/cli_utils.py 2026-03-10 18:46:56.000000000 +0100 @@ -98,45 +98,31 @@ def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, values: str | Sequence[Any] | None, option_string: str | None = None) -> None: - """Just wrap add_domains in argparseese.""" match values: case str(): - add_domains(namespace, str(values)) + for domain in values.split(","): + add_dns_name(namespace, san.DNSName(domain.strip())) case _: # https://docs.python.org/3/library/argparse.html#nargs raise TypeError("shouldn't happen: non-str passed by argparse when nargs=None") -def add_domains(args_or_config: Union[argparse.Namespace, configuration.NamespaceConfig], - domains: Optional[str]) -> list[san.DNSName]: - """Registers new domains to be used during the current client run. +def add_dns_name(args_or_config: Union[argparse.Namespace, configuration.NamespaceConfig], + dns_name: san.DNSName) -> None: + """Registers a new domain to be used during the current client run. - Domains are not added to the list of requested domains if they have - already been registered. + The domain is not added if it has already been registered. :param args_or_config: parsed command line arguments :type args_or_config: argparse.Namespace or configuration.NamespaceConfig - :param str domain: one or more comma separated domains - - :returns: domains after they have been normalized and validated - :rtype: `list` of `str` - + :param san.DNSName dns_name: a DNS name """ - validated_domains: list[san.DNSName] = [] - if not domains: - return validated_domains - - for d in domains.split(","): - domain = san.DNSName(d.strip()) - validated_domains.append(domain) - if domain not in args_or_config.domains: - args_or_config.domains.append(domain) + if dns_name not in args_or_config.domains: + args_or_config.domains.append(dns_name) - return validated_domains - -class _IPAddressAction(argparse.Action): +class IPAddressAction(argparse.Action): """Action class for parsing IP addresses.""" def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, @@ -145,12 +131,26 @@ match values: case str(): # This will throw an exception if the IP address doesn't parse. - namespace.ip_addresses.append(san.IPAddress(values)) + add_ip_address(namespace, san.IPAddress(values)) case _: # https://docs.python.org/3/library/argparse.html#nargs raise TypeError("shouldn't happen: non-str passed by argparse when nargs=None") +def add_ip_address(args_or_config: Union[argparse.Namespace, configuration.NamespaceConfig], + ip_address: san.IPAddress) -> None: + """Registers a new IP address to be used during the current client run. + + The IP address is not added if it has already been registered. + + :param args_or_config: parsed command line arguments + :type args_or_config: argparse.Namespace or + configuration.NamespaceConfig + :param san.IPAddress ip_address: an IP address + """ + if ip_address not in args_or_config.ip_addresses: + args_or_config.ip_addresses.append(ip_address) + class CaseInsensitiveList(list): """A list that will ignore case when searching. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-5.3.1/src/certbot/_internal/plugins/webroot.py new/certbot-5.4.0/src/certbot/_internal/plugins/webroot.py --- old/certbot-5.3.1/src/certbot/_internal/plugins/webroot.py 2026-02-09 22:19:24.000000000 +0100 +++ new/certbot-5.4.0/src/certbot/_internal/plugins/webroot.py 2026-03-10 18:46:56.000000000 +0100 @@ -15,6 +15,7 @@ from certbot import errors from certbot import interfaces from certbot._internal import cli +from certbot._internal import san from certbot.achallenges import AnnotatedChallenge from certbot.compat import filesystem from certbot.compat import os @@ -69,14 +70,14 @@ def add_parser_arguments(cls, add: Callable[..., None]) -> None: add("path", "-w", default=[], action=_WebrootPathAction, help="public_html / webroot path. This can be specified multiple " - "times to handle different domains; each domain will have " + "times to handle different identifiers; each identifier will have " "the webroot path that preceded it. For instance: `-w " "/var/www/example -d example.com -d www.example.com -w " "/var/www/thing -d thing.net -d m.thing.net` (default: Ask)") add("map", default={}, action=_WebrootMapAction, - help="JSON dictionary mapping domains to webroot paths; this " - "implies -d for each entry. You may need to escape this from " - "your shell. E.g.: --webroot-map " + help="JSON dictionary mapping identifiers to webroot paths; this " + "implies -d or --ip-address for each entry. You may need to " + " escape this from your shell. E.g.: --webroot-map " '\'{"eg1.is,m.eg1.is":"/www/eg1/", "eg2.is":"/www/eg2"}\' ' "This option is merged with, but takes precedence over, -w / " "-d entries. At present, if you put webroot-map in a config " @@ -85,7 +86,7 @@ def auth_hint(self, failed_achalls: list[AnnotatedChallenge]) -> str: # pragma: no cover return ("The Certificate Authority failed to download the temporary challenge files " - "created by Certbot. Ensure that the listed domains serve their content from " + "created by Certbot. Ensure that the listed identifiers serve their content from " "the provided --webroot-path/-w and that files created there can be downloaded " "from the internet.") @@ -106,9 +107,6 @@ pass def perform(self, achalls: list[AnnotatedChallenge]) -> list[challenges.ChallengeResponse]: # pylint: disable=missing-function-docstring - if any(achall.identifier.typ == messages.IDENTIFIER_IP for achall in achalls): - raise errors.ConfigurationError( - "webroot authenticator not supported for IP address certificates") self._set_webroots(achalls) self._create_challenge_dirs() @@ -118,7 +116,7 @@ def _set_webroots(self, achalls: Iterable[AnnotatedChallenge]) -> None: if self.conf("path"): webroot_path = self.conf("path")[-1] - logger.info("Using the webroot path %s for all unmatched domains.", + logger.info("Using the webroot path %s for all unmatched identifiers.", webroot_path) for achall in achalls: self.conf("map").setdefault(achall.identifier.value, webroot_path) @@ -126,7 +124,7 @@ known_webroots = list(set(self.conf("map").values())) for achall in achalls: if achall.identifier.value not in self.conf("map"): - new_webroot = self._prompt_for_webroot(achall.identifier.value, + new_webroot = self._prompt_for_webroot(achall.identifier, known_webroots) # Put the most recently input # webroot first for easy selection @@ -137,46 +135,48 @@ known_webroots.insert(0, new_webroot) self.conf("map")[achall.identifier.value] = new_webroot - def _prompt_for_webroot(self, domain: str, known_webroots: list[str]) -> Optional[str]: + def _prompt_for_webroot(self, identifier: messages.Identifier, + known_webroots: list[str]) -> Optional[str]: webroot = None while webroot is None: if known_webroots: # Only show the menu if we have options for it - webroot = self._prompt_with_webroot_list(domain, known_webroots) + webroot = self._prompt_with_webroot_list(identifier, known_webroots) if webroot is None: - webroot = self._prompt_for_new_webroot(domain) + webroot = self._prompt_for_new_webroot(identifier) else: # Allow prompt to raise PluginError instead of looping forever - webroot = self._prompt_for_new_webroot(domain, True) + webroot = self._prompt_for_new_webroot(identifier, True) return webroot - def _prompt_with_webroot_list(self, domain: str, + def _prompt_with_webroot_list(self, identifier: messages.Identifier, known_webroots: list[str]) -> Optional[str]: path_flag = "--" + self.option_name("path") while True: code, index = display_util.menu( - "Select the webroot for {0}:".format(domain), + "Select the webroot for {0}:".format(identifier.value), ["Enter a new webroot"] + known_webroots, cli_flag=path_flag, force_interactive=True) if code == display_util.CANCEL: raise errors.PluginError( - "Every requested domain must have a " + "Every requested identifier must have a " "webroot when using the webroot plugin.") return None if index == 0 else known_webroots[index - 1] # code == display_util.OK - def _prompt_for_new_webroot(self, domain: str, allowraise: bool = False) -> Optional[str]: + def _prompt_for_new_webroot(self, identifier: messages.Identifier, + allowraise: bool = False) -> Optional[str]: code, webroot = ops.validated_directory( _validate_webroot, - "Input the webroot for {0}:".format(domain), + "Input the webroot for {0}:".format(identifier.value), force_interactive=True) if code == display_util.CANCEL: if not allowraise: return None raise errors.PluginError( - "Every requested domain must have a " + "Every requested identifier must have a " "webroot when using the webroot plugin.") return _validate_webroot(webroot) # code == display_util.OK @@ -184,9 +184,10 @@ path_map = self.conf("map") if not path_map: raise errors.PluginError( - "Missing parts of webroot configuration; please set either " - "--webroot-path and --domains, or --webroot-map. Run with " - " --help webroot for examples.") + "Missing parts of webroot configuration; please set " + "--webroot-path and --domains or --ip-address. " + "Alternatively you may set --webroot-map. " + "Run with --help webroot for examples.") for name, path in path_map.items(): self.full_roots[name] = os.path.join(path, os.path.normcase( challenges.HTTP01.URI_ROOT_PATH)) @@ -295,10 +296,16 @@ option_string: Optional[str] = None) -> None: if webroot_map is None: return - for domains, webroot_path in json.loads(str(webroot_map)).items(): + for identlist, webroot_path in json.loads(str(webroot_map)).items(): webroot_path = _validate_webroot(webroot_path) - namespace.webroot_map.update( - (d.dns_name, webroot_path) for d in cli.add_domains(namespace, domains)) + for s in san.guess(identlist.split(",")): + match s: + case san.IPAddress(): + cli.add_ip_address(namespace, s) + case san.DNSName(): + cli.add_dns_name(namespace, s) + + namespace.webroot_map[str(s)] = webroot_path class _WebrootPathAction(argparse.Action): @@ -306,17 +313,17 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self._domain_before_webroot = False + self._ident_before_webroot = False def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, webroot_path: Union[str, Sequence[Any], None], option_string: Optional[str] = None) -> None: if webroot_path is None: return - if self._domain_before_webroot: + if self._ident_before_webroot: raise errors.PluginError( "If you specify multiple webroot paths, " - "one of them must precede all domain flags") + "one of them must precede all --domain and --ip-address flags") if namespace.webroot_path: # Apply previous webroot to all matched @@ -324,8 +331,10 @@ prev_webroot = namespace.webroot_path[-1] for domain in namespace.domains: namespace.webroot_map.setdefault(domain.dns_name, prev_webroot) - elif namespace.domains: - self._domain_before_webroot = True + for ip_address in namespace.ip_addresses: + namespace.webroot_map.setdefault(str(ip_address), prev_webroot) + elif namespace.domains or namespace.ip_addresses: + self._ident_before_webroot = True namespace.webroot_path.append(_validate_webroot(str(webroot_path))) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-5.3.1/src/certbot/_internal/renewal.py new/certbot-5.4.0/src/certbot/_internal/renewal.py --- old/certbot-5.3.1/src/certbot/_internal/renewal.py 2026-02-09 22:19:24.000000000 +0100 +++ new/certbot-5.4.0/src/certbot/_internal/renewal.py 2026-03-10 18:46:56.000000000 +0100 @@ -632,16 +632,19 @@ def handle_renewal_request(config: configuration.NamespaceConfig) -> None: """Examine each lineage; renew if due and report results""" - # This is trivially False if config.domains is empty - if any(domain.dns_name not in config.webroot_map for domain in config.domains): - # If more plugins start using cli.add_domains, + sans: list[san.SAN] = config.domains + config.ip_addresses + + # This is trivially False if sans is empty + if any(str(san) not in config.webroot_map for san in sans): + # If more plugins start using cli.add_domain / cli.add_ip_address, # we may want to only log a warning here raise errors.Error("Currently, the renew verb is capable of either " "renewing all installed certificates that are due " "to be renewed or renewing a single certificate specified " - "by its name. If you would like to renew specific " - "certificates by their domains, use the certonly command " - "instead. The renew verb may provide other options " + "by its name using the --cert-name option (-d, --domain, and " + "--ip-address are not valid options for the renew subcommand). If you " + "would like to renew specific certificates by their identifiers, use " + "the certonly command instead. The renew verb may provide other options " "for selecting certificates to renew in the future.") if config.certname: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-5.3.1/src/certbot/_internal/tests/plugins/webroot_test.py new/certbot-5.4.0/src/certbot/_internal/tests/plugins/webroot_test.py --- old/certbot-5.3.1/src/certbot/_internal/tests/plugins/webroot_test.py 2026-02-09 22:19:24.000000000 +0100 +++ new/certbot-5.4.0/src/certbot/_internal/tests/plugins/webroot_test.py 2026-03-10 18:46:56.000000000 +0100 @@ -317,18 +317,33 @@ identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value="thing.com"), account_key=KEY) + ipchall = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.HTTP01(token=((b'a' * 16))), + messages.STATUS_PENDING), + identifier=messages.Identifier(typ=messages.IDENTIFIER_IP, value="1.2.3.4"), + account_key=KEY) + def setUp(self): from certbot._internal.plugins.webroot import Authenticator self.path = tempfile.mkdtemp() self.parser = argparse.ArgumentParser() + self.parser.ip_addresses = [] self.parser.add_argument("-d", "--domains", action=cli_utils.DomainsAction, default=[]) + self.parser.add_argument("--ip-address", + action=cli_utils.IPAddressAction, + dest="ip_addresses", + default=[]) Authenticator.inject_parser_options(self.parser, "webroot") def test_webroot_map_action(self): + other_path = tempfile.mkdtemp() args = self.parser.parse_args( - ["--webroot-map", json.dumps({'thing.com': self.path})]) + ["--webroot-map", json.dumps({'thing.com,thunk.com,9.8.7.6': self.path,'thunk.com': other_path})]) assert args.webroot_map["thing.com"] == self.path + assert args.webroot_map["9.8.7.6"] == self.path + assert args.webroot_map["thunk.com"] == other_path def test_domain_before_webroot(self): args = self.parser.parse_args( @@ -336,6 +351,15 @@ config = self._get_config_after_perform(args) assert config.webroot_map[self.achall.identifier.value] == self.path + def test_multi_identifier(self): + args = self.parser.parse_args( + "-w {0} -d {1} --ip-address {2}".format( + self.path, self.achall.identifier.value, self.ipchall.identifier.value).split()) + + config = self._get_config_after_perform(args, challs=[self.achall, self.ipchall]) + assert config.webroot_map[self.achall.identifier.value] == self.path + assert config.webroot_map[self.ipchall.identifier.value] == self.path + def test_domain_before_webroot_error(self): with pytest.raises(errors.PluginError): self.parser.parse_args("-d foo -w bar -w baz".split()) @@ -343,11 +367,26 @@ self.parser.parse_args("-d foo -w bar -d baz -w qux".split()) def test_multiwebroot(self): - args = self.parser.parse_args("-w {0} -d {1} -w {2} -d bar".format( - self.path, self.achall.identifier.value, tempfile.mkdtemp()).split()) - assert args.webroot_map[self.achall.identifier.value] == self.path - config = self._get_config_after_perform(args) - assert config.webroot_map[self.achall.identifier.value] == self.path + ip = self.ipchall.identifier.value + dns_name = self.achall.identifier.value + + ip_path = tempfile.mkdtemp() + dns_path = tempfile.mkdtemp() + args = self.parser.parse_args(f"-w {dns_path} -d {dns_name} -w {ip_path} --ip-address {ip}".split()) + config = self._get_config_after_perform(args, challs=[self.achall, self.ipchall]) + assert config.webroot_map[dns_name] == dns_path + assert config.webroot_map[ip] == ip_path + + def test_multiwebroot_ip_first(self): + ip = self.ipchall.identifier.value + dns_name = self.achall.identifier.value + + ip_path = tempfile.mkdtemp() + dns_path = tempfile.mkdtemp() + args = self.parser.parse_args(f"-w {ip_path} --ip-address {ip} -w {dns_path} -d {dns_name}".split()) + config = self._get_config_after_perform(args, challs=[self.achall, self.ipchall]) + assert config.webroot_map[dns_name] == dns_path + assert config.webroot_map[ip] == ip_path def test_webroot_map_partial_without_perform(self): # This test acknowledges the fact that webroot_map content will be partial if webroot @@ -362,10 +401,12 @@ assert args.webroot_map == {self.achall.identifier.value: self.path} assert args.webroot_path == [self.path, other_webroot_path] - def _get_config_after_perform(self, config): + def _get_config_after_perform(self, config, challs=None): + if not challs: + challs = [self.achall] from certbot._internal.plugins.webroot import Authenticator auth = Authenticator(config, "webroot") - auth.perform([self.achall]) + auth.perform(challs) return auth.config diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-5.3.1/src/certbot/_internal/tests/reverter_test.py new/certbot-5.4.0/src/certbot/_internal/tests/reverter_test.py --- old/certbot-5.3.1/src/certbot/_internal/tests/reverter_test.py 2026-02-09 22:19:24.000000000 +0100 +++ new/certbot-5.4.0/src/certbot/_internal/tests/reverter_test.py 2026-03-10 18:46:56.000000000 +0100 @@ -358,7 +358,7 @@ # Test Generic warning self._setup_three_checkpoints() - mock_logger.warning.call_count = 0 + mock_logger.warning.reset_mock() self.reverter.rollback_checkpoints(4) assert mock_logger.warning.call_count == 1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-5.3.1/src/certbot.egg-info/PKG-INFO new/certbot-5.4.0/src/certbot.egg-info/PKG-INFO --- old/certbot-5.3.1/src/certbot.egg-info/PKG-INFO 2026-02-09 22:19:26.000000000 +0100 +++ new/certbot-5.4.0/src/certbot.egg-info/PKG-INFO 2026-03-10 18:46:57.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: certbot -Version: 5.3.1 +Version: 5.4.0 Summary: ACME client Author: Certbot Project License-Expression: Apache-2.0 @@ -25,7 +25,7 @@ Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE.txt -Requires-Dist: acme>=5.3.1 +Requires-Dist: acme>=5.4.0 Requires-Dist: ConfigArgParse>=1.5.3 Requires-Dist: configobj>=5.0.6 Requires-Dist: cryptography>=43.0.0 @@ -156,3 +156,10 @@ * Configuration changes are logged and can be reverted. .. Do not modify this comment unless you know what you're doing. tag:features-end + +Thanks +------ + +We appreciate `Digital Ocean`_ for donating credits to help us test and develop Certbot. + +.. _Digital Ocean: https://www.digitalocean.com/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-5.3.1/src/certbot.egg-info/requires.txt new/certbot-5.4.0/src/certbot.egg-info/requires.txt --- old/certbot-5.3.1/src/certbot.egg-info/requires.txt 2026-02-09 22:19:26.000000000 +0100 +++ new/certbot-5.4.0/src/certbot.egg-info/requires.txt 2026-03-10 18:46:57.000000000 +0100 @@ -1,4 +1,4 @@ -acme>=5.3.1 +acme>=5.4.0 ConfigArgParse>=1.5.3 configobj>=5.0.6 cryptography>=43.0.0
