Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-certbot-dns-route53 for
openSUSE:Factory checked in at 2025-06-16 12:26:31
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-certbot-dns-route53 (Old)
and /work/SRC/openSUSE:Factory/.python-certbot-dns-route53.new.19631 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-certbot-dns-route53"
Mon Jun 16 12:26:31 2025 rev:46 rq:1286017 version:4.1.1
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-certbot-dns-route53/python-certbot-dns-route53.changes
2025-04-22 17:29:58.171331834 +0200
+++
/work/SRC/openSUSE:Factory/.python-certbot-dns-route53.new.19631/python-certbot-dns-route53.changes
2025-06-16 12:26:38.100659717 +0200
@@ -1,0 +2,7 @@
+Fri Jun 13 14:52:31 UTC 2025 - Markéta Machová <[email protected]>
+
+- update to version 4.1.1
+ * Switched to src-layout from flat-layout to accommodate PEP 517 pip
+ editable installs
+
+-------------------------------------------------------------------
Old:
----
certbot_dns_route53-4.0.0.tar.gz
New:
----
certbot_dns_route53-4.1.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-certbot-dns-route53.spec ++++++
--- /var/tmp/diff_new_pack.yh9D1L/_old 2025-06-16 12:26:38.696684536 +0200
+++ /var/tmp/diff_new_pack.yh9D1L/_new 2025-06-16 12:26:38.696684536 +0200
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-certbot-dns-route53
-Version: 4.0.0
+Version: 4.1.1
Release: 0
Summary: Route53 DNS Authenticator plugin for Certbot
License: Apache-2.0
++++++ certbot_dns_route53-4.0.0.tar.gz -> certbot_dns_route53-4.1.1.tar.gz
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/certbot_dns_route53-4.0.0/MANIFEST.in
new/certbot_dns_route53-4.1.1/MANIFEST.in
--- old/certbot_dns_route53-4.0.0/MANIFEST.in 2025-04-08 00:03:33.000000000
+0200
+++ new/certbot_dns_route53-4.1.1/MANIFEST.in 2025-06-12 20:08:34.000000000
+0200
@@ -1,6 +1,6 @@
include LICENSE.txt
include README.rst
recursive-include docs *
-include certbot_dns_route53/py.typed
+include src/certbot_dns_route53/py.typed
global-exclude __pycache__
global-exclude *.py[cod]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/certbot_dns_route53-4.0.0/PKG-INFO
new/certbot_dns_route53-4.1.1/PKG-INFO
--- old/certbot_dns_route53-4.0.0/PKG-INFO 2025-04-08 00:03:41.034472200
+0200
+++ new/certbot_dns_route53-4.1.1/PKG-INFO 2025-06-12 20:08:43.940638000
+0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: certbot-dns-route53
-Version: 4.0.0
+Version: 4.1.1
Summary: Route53 DNS Authenticator plugin for Certbot
Home-page: https://github.com/certbot/certbot
Author: Certbot Project
@@ -25,11 +25,11 @@
Classifier: Topic :: System :: Networking
Classifier: Topic :: System :: Systems Administration
Classifier: Topic :: Utilities
-Requires-Python: >=3.9
+Requires-Python: >=3.9.2
License-File: LICENSE.txt
Requires-Dist: boto3>=1.15.15
-Requires-Dist: acme>=4.0.0
-Requires-Dist: certbot>=4.0.0
+Requires-Dist: acme>=4.1.1
+Requires-Dist: certbot>=4.1.1
Provides-Extra: docs
Requires-Dist: Sphinx>=1.0; extra == "docs"
Requires-Dist: sphinx_rtd_theme; extra == "docs"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/certbot_dns_route53/__init__.py
new/certbot_dns_route53-4.1.1/certbot_dns_route53/__init__.py
--- old/certbot_dns_route53-4.0.0/certbot_dns_route53/__init__.py
2025-04-08 00:03:33.000000000 +0200
+++ new/certbot_dns_route53-4.1.1/certbot_dns_route53/__init__.py
1970-01-01 01:00:00.000000000 +0100
@@ -1,105 +0,0 @@
-"""
-The `~certbot_dns_route53.dns_route53` plugin automates the process of
-completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
-subsequently removing, TXT records using the Amazon Web Services Route 53 API.
-
-.. note::
- The plugin is not installed by default. It can be installed by heading to
- `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_,
choosing your system and
- selecting the Wildcard tab.
-
-Credentials
------------
-Use of this plugin requires a configuration file containing Amazon Web Sevices
-API credentials for an account with the following permissions:
-
-* ``route53:ListHostedZones``
-* ``route53:GetChange``
-* ``route53:ChangeResourceRecordSets``
-
-These permissions can be captured in an AWS policy like the one below. Amazon
-provides `information about managing access
<https://docs.aws.amazon.com/Route53
-/latest/DeveloperGuide/access-control-overview.html>`_ and `information about
-the required permissions <https://docs.aws.amazon.com/Route53/latest
-/DeveloperGuide/r53-api-permissions-ref.html>`_
-
-.. code-block:: json
- :name: sample-aws-policy.json
- :caption: Example AWS policy file:
-
- {
- "Version": "2012-10-17",
- "Id": "certbot-dns-route53 sample policy",
- "Statement": [
- {
- "Effect": "Allow",
- "Action": [
- "route53:ListHostedZones",
- "route53:GetChange"
- ],
- "Resource": [
- "*"
- ]
- },
- {
- "Effect" : "Allow",
- "Action" : [
- "route53:ChangeResourceRecordSets"
- ],
- "Resource" : [
- "arn:aws:route53:::hostedzone/YOURHOSTEDZONEID"
- ]
- }
- ]
- }
-
-The `access keys <https://docs.aws.amazon.com/general/latest/gr
-/aws-sec-cred-types.html#access-keys-and-secret-access-keys>`_ for an account
-with these permissions must be supplied in one of the following ways, which are
-discussed in more detail in the Boto3 library's documentation about
`configuring
-credentials <https://boto3.readthedocs.io/en/latest/guide/configuration.html
-#best-practices-for-configuring-credentials>`_.
-
-* Using the ``AWS_ACCESS_KEY_ID`` and ``AWS_SECRET_ACCESS_KEY`` environment
- variables.
-* Using a credentials configuration file at the default location,
- ``~/.aws/credentials``. If you're running on sudo, the credentials
- will be picked up from the root home.
-* Using a credentials configuration file at a path supplied using the
- ``AWS_CONFIG_FILE`` environment variable.
-
-.. code-block:: ini
- :name: config.ini
- :caption: Example credentials config file:
-
- [default]
- aws_access_key_id=AKIAIOSFODNN7EXAMPLE
- aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
-
-.. caution::
- You should protect these API credentials as you would a password. Users who
- can read this file can use these credentials to issue some types of API
calls
- on your behalf, limited by the permissions assigned to the account. Users
who
- can cause Certbot to run using these credentials can complete a ``dns-01``
- challenge to acquire new certificates or revoke existing certificates for
- domains these credentials are authorized to manage.
-
-
-Examples
---------
-.. code-block:: bash
- :caption: To acquire a certificate for ``example.com``
-
- certbot certonly \\
- --dns-route53 \\
- -d example.com
-
-.. code-block:: bash
- :caption: To acquire a single certificate for both ``example.com`` and
- ``www.example.com``
-
- certbot certonly \\
- --dns-route53 \\
- -d example.com \\
- -d www.example.com
-"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/certbot_dns_route53/_internal/__init__.py
new/certbot_dns_route53-4.1.1/certbot_dns_route53/_internal/__init__.py
--- old/certbot_dns_route53-4.0.0/certbot_dns_route53/_internal/__init__.py
2025-04-08 00:03:33.000000000 +0200
+++ new/certbot_dns_route53-4.1.1/certbot_dns_route53/_internal/__init__.py
1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-"""Internal implementation of `~certbot_dns_route53.dns_route53` plugin."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/certbot_dns_route53/_internal/dns_route53.py
new/certbot_dns_route53-4.1.1/certbot_dns_route53/_internal/dns_route53.py
--- old/certbot_dns_route53-4.0.0/certbot_dns_route53/_internal/dns_route53.py
2025-04-08 00:03:33.000000000 +0200
+++ new/certbot_dns_route53-4.1.1/certbot_dns_route53/_internal/dns_route53.py
1970-01-01 01:00:00.000000000 +0100
@@ -1,191 +0,0 @@
-"""Certbot Route53 authenticator plugin."""
-import collections
-import logging
-import time
-from typing import Any
-from typing import Callable
-from typing import DefaultDict
-from typing import Dict
-from typing import Iterable
-from typing import List
-from typing import Type
-from typing import cast
-
-import boto3
-from botocore.exceptions import ClientError
-from botocore.exceptions import NoCredentialsError
-
-from acme import challenges
-from certbot import achallenges
-from certbot import errors
-from certbot import interfaces
-from certbot.achallenges import AnnotatedChallenge
-from certbot.plugins import common
-
-logger = logging.getLogger(__name__)
-
-INSTRUCTIONS = (
- "To use certbot-dns-route53, configure credentials as described at "
-
"https://boto3.readthedocs.io/en/latest/guide/configuration.html#best-practices-for-configuring-credentials
" # pylint: disable=line-too-long
- "and add the necessary permissions for Route53 access.")
-
-
-class Authenticator(common.Plugin, interfaces.Authenticator):
- """Route53 Authenticator
-
- This authenticator solves a DNS01 challenge by uploading the answer to AWS
- Route53.
- """
-
- description = ("Obtain certificates using a DNS TXT record (if you are
using AWS Route53 for "
- "DNS).")
- ttl = 10
-
- def __init__(self, *args: Any, **kwargs: Any) -> None:
- super().__init__(*args, **kwargs)
- self.r53 = boto3.client("route53")
- self._attempt_cleanup = False
- self._resource_records: DefaultDict[str, List[Dict[str, str]]] = \
- collections.defaultdict(list)
-
- def more_info(self) -> str:
- return "Solve a DNS01 challenge using AWS Route53"
-
- @classmethod
- def add_parser_arguments(cls, add: Callable[..., None]) -> None:
- # This authenticator currently adds no extra arguments.
- pass
-
- def auth_hint(self, failed_achalls: List[achallenges.AnnotatedChallenge])
-> str:
- return (
- 'The Certificate Authority failed to verify the DNS TXT records
created by '
- '--dns-route53. Ensure the above domains have their DNS hosted by
AWS Route53.'
- )
-
- def prepare(self) -> None:
- pass
-
- def get_chall_pref(self, unused_domain: str) ->
Iterable[Type[challenges.Challenge]]:
- return [challenges.DNS01]
-
- def perform(self, achalls: List[AnnotatedChallenge]) ->
List[challenges.ChallengeResponse]:
- self._attempt_cleanup = True
-
- try:
- change_ids = [
- self._change_txt_record("UPSERT",
- achall.validation_domain_name(achall.domain),
- achall.validation(achall.account_key))
- for achall in achalls
- ]
-
- for change_id in change_ids:
- self._wait_for_change(change_id)
- except (NoCredentialsError, ClientError) as e:
- logger.debug('Encountered error during perform: %s', e,
exc_info=True)
- raise errors.PluginError("\n".join([str(e), INSTRUCTIONS]))
- return [achall.response(achall.account_key) for achall in achalls]
-
- def cleanup(self, achalls: List[achallenges.AnnotatedChallenge]) -> None:
- if self._attempt_cleanup:
- for achall in achalls:
- domain = achall.domain
- validation_domain_name = achall.validation_domain_name(domain)
- validation = achall.validation(achall.account_key)
-
- self._cleanup(validation_domain_name, validation)
-
- def _cleanup(self, validation_name: str, validation: str) -> None:
- try:
- self._change_txt_record("DELETE", validation_name, validation)
- except (NoCredentialsError, ClientError) as e:
- logger.debug('Encountered error during cleanup: %s', e,
exc_info=True)
-
- def _find_zone_id_for_domain(self, domain: str) -> str:
- """Find the zone id responsible a given FQDN.
-
- That is, the id for the zone whose name is the longest parent of the
- domain.
- """
- paginator = self.r53.get_paginator("list_hosted_zones")
- zones: list[tuple[str, str]] = []
- target_labels = domain.rstrip(".").split(".")
- for page in paginator.paginate():
- for zone in page["HostedZones"]:
- if zone["Config"]["PrivateZone"]:
- continue
-
- candidate_labels = zone["Name"].rstrip(".").split(".")
- if candidate_labels == target_labels[-len(candidate_labels):]:
- zones.append((zone["Name"], zone["Id"]))
-
- if not zones:
- raise errors.PluginError(
- "Unable to find a Route53 hosted zone for {0}".format(domain)
- )
-
- # Order the zones that are suffixes for our desired to domain by
- # length, this puts them in an order like:
- # ["foo.bar.baz.com", "bar.baz.com", "baz.com", "com"]
- # And then we choose the first one, which will be the most specific.
- zones.sort(key=lambda z: len(z[0]), reverse=True)
- return zones[0][1]
-
- def _change_txt_record(self, action: str, validation_domain_name: str,
validation: str) -> str:
- zone_id = self._find_zone_id_for_domain(validation_domain_name)
-
- rrecords = self._resource_records[validation_domain_name]
- challenge = {"Value": '"{0}"'.format(validation)}
- if action == "DELETE":
- # Remove the record being deleted from the list of tracked records
- rrecords.remove(challenge)
- if rrecords:
- # Need to update instead, as we're not deleting the rrset
- action = "UPSERT"
- else:
- # Create a new list containing the record to use with DELETE
- rrecords = [challenge]
- else:
- rrecords.append(challenge)
-
- response = self.r53.change_resource_record_sets(
- HostedZoneId=zone_id,
- ChangeBatch={
- "Comment": "certbot-dns-route53 certificate validation " +
action,
- "Changes": [
- {
- "Action": action,
- "ResourceRecordSet": {
- "Name": validation_domain_name,
- "Type": "TXT",
- "TTL": self.ttl,
- "ResourceRecords": rrecords,
- }
- }
- ]
- }
- )
- return cast(str, response["ChangeInfo"]["Id"])
-
- def _wait_for_change(self, change_id: str) -> None:
- """Wait for a change to be propagated to all Route53 DNS servers.
-
https://docs.aws.amazon.com/Route53/latest/APIReference/API_GetChange.html
- """
- for unused_n in range(0, 120):
- response = self.r53.get_change(Id=change_id)
- if response["ChangeInfo"]["Status"] == "INSYNC":
- return
- time.sleep(5)
- raise errors.PluginError(
- "Timed out waiting for Route53 change. Current status: %s" %
- response["ChangeInfo"]["Status"])
-
-
-# Our route53 plugin was initially a 3rd party plugin named
`certbot-route53:auth` as described at
-# https://github.com/certbot/certbot/issues/4688. This shim exists to allow
installations using the
-# old plugin name of `certbot-route53:auth` to continue to work without
cluttering things like
-# Certbot's help output with two route53 plugins.
-class HiddenAuthenticator(Authenticator):
- """A hidden shim around certbot-dns-route53 for backwards compatibility."""
-
- hidden = True
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/certbot_dns_route53/_internal/tests/__init__.py
new/certbot_dns_route53-4.1.1/certbot_dns_route53/_internal/tests/__init__.py
---
old/certbot_dns_route53-4.0.0/certbot_dns_route53/_internal/tests/__init__.py
2025-04-08 00:03:33.000000000 +0200
+++
new/certbot_dns_route53-4.1.1/certbot_dns_route53/_internal/tests/__init__.py
1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-"""certbot-dns-route53 tests"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/certbot_dns_route53/_internal/tests/dns_route53_test.py
new/certbot_dns_route53-4.1.1/certbot_dns_route53/_internal/tests/dns_route53_test.py
---
old/certbot_dns_route53-4.0.0/certbot_dns_route53/_internal/tests/dns_route53_test.py
2025-04-08 00:03:33.000000000 +0200
+++
new/certbot_dns_route53-4.1.1/certbot_dns_route53/_internal/tests/dns_route53_test.py
1970-01-01 01:00:00.000000000 +0100
@@ -1,273 +0,0 @@
-"""Tests for certbot_dns_route53._internal.dns_route53.Authenticator"""
-
-import sys
-import unittest
-from unittest import mock
-
-from botocore.exceptions import ClientError
-from botocore.exceptions import NoCredentialsError
-import josepy as jose
-import pytest
-
-from acme import challenges
-from certbot import achallenges
-from certbot import errors
-from certbot.compat import os
-from certbot.plugins.dns_test_common import DOMAIN
-from certbot.tests import acme_util
-from certbot.tests import util as test_util
-
-DOMAIN = 'example.com'
-KEY = jose.jwk.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
-
-
-class AuthenticatorTest(unittest.TestCase):
- # pylint: disable=protected-access
-
- achall = achallenges.KeyAuthorizationAnnotatedChallenge(
- challb=acme_util.DNS01, domain=DOMAIN, account_key=KEY)
-
- def setUp(self):
- from certbot_dns_route53._internal.dns_route53 import Authenticator
-
- super().setUp()
-
- self.config = mock.MagicMock()
-
- # Set up dummy credentials for testing
- os.environ["AWS_ACCESS_KEY_ID"] = "dummy_access_key"
- os.environ["AWS_SECRET_ACCESS_KEY"] = "dummy_secret_access_key"
-
- self.auth = Authenticator(self.config, "route53")
-
- def tearDown(self):
- # Remove the dummy credentials from env vars
- del os.environ["AWS_ACCESS_KEY_ID"]
- del os.environ["AWS_SECRET_ACCESS_KEY"]
-
- def test_more_info(self) -> None:
- self.assertTrue(isinstance(self.auth.more_info(), str))
-
- def test_get_chall_pref(self) -> None:
- self.assertEqual(self.auth.get_chall_pref("example.org"),
[challenges.DNS01])
-
- def test_perform(self):
- self.auth._change_txt_record = mock.MagicMock() # type:
ignore[method-assign, unused-ignore]
- self.auth._wait_for_change = mock.MagicMock() # type: ignore
[method-assign, unused-ignore]
-
- self.auth.perform([self.achall])
-
- self.auth._change_txt_record.assert_called_once_with("UPSERT",
-
'_acme-challenge.' + DOMAIN,
- mock.ANY)
- assert self.auth._wait_for_change.call_count == 1
-
- def test_perform_no_credentials_error(self):
- self.auth._change_txt_record = mock.MagicMock( # type: ignore
[method-assign, unused-ignore]
- side_effect=NoCredentialsError)
-
- with pytest.raises(errors.PluginError):
- self.auth.perform([self.achall])
-
- def test_perform_client_error(self):
- self.auth._change_txt_record = mock.MagicMock( # type: ignore
[method-assign, unused-ignore]
- side_effect=ClientError({"Error": {"Code": "foo"}}, "bar"))
-
- with pytest.raises(errors.PluginError):
- self.auth.perform([self.achall])
-
- def test_cleanup(self):
- self.auth._attempt_cleanup = True
-
- self.auth._change_txt_record = mock.MagicMock() # type:
ignore[method-assign, unused-ignore]
-
- self.auth.cleanup([self.achall])
-
- self.auth._change_txt_record.assert_called_once_with("DELETE",
-
'_acme-challenge.'+DOMAIN,
- mock.ANY)
-
- def test_cleanup_no_credentials_error(self):
- self.auth._attempt_cleanup = True
-
- self.auth._change_txt_record = mock.MagicMock( # type: ignore
[method-assign, unused-ignore]
- side_effect=NoCredentialsError)
-
- self.auth.cleanup([self.achall])
-
- def test_cleanup_client_error(self):
- self.auth._attempt_cleanup = True
-
- self.auth._change_txt_record = mock.MagicMock( # type: ignore
[method-assign, unused-ignore]
- side_effect=ClientError({"Error": {"Code": "foo"}}, "bar"))
-
- self.auth.cleanup([self.achall])
-
-
-class ClientTest(unittest.TestCase):
- # pylint: disable=protected-access
-
- PRIVATE_ZONE = {
- "Id": "BAD-PRIVATE",
- "Name": "example.com",
- "Config": {
- "PrivateZone": True
- }
- }
-
- EXAMPLE_NET_ZONE = {
- "Id": "BAD-WRONG-TLD",
- "Name": "example.net",
- "Config": {
- "PrivateZone": False
- }
- }
-
- EXAMPLE_COM_ZONE = {
- "Id": "EXAMPLE",
- "Name": "example.com",
- "Config": {
- "PrivateZone": False
- }
- }
-
- FOO_EXAMPLE_COM_ZONE = {
- "Id": "FOO",
- "Name": "foo.example.com",
- "Config": {
- "PrivateZone": False
- }
- }
-
- def setUp(self):
- from certbot_dns_route53._internal.dns_route53 import Authenticator
-
- self.config = mock.MagicMock()
-
- # Set up dummy credentials for testing
- os.environ["AWS_ACCESS_KEY_ID"] = "dummy_access_key"
- os.environ["AWS_SECRET_ACCESS_KEY"] = "dummy_secret_access_key"
-
- self.client = Authenticator(self.config, "route53")
-
- def tearDown(self):
- # Remove the dummy credentials from env vars
- del os.environ["AWS_ACCESS_KEY_ID"]
- del os.environ["AWS_SECRET_ACCESS_KEY"]
-
- def test_find_zone_id_for_domain(self):
- self.client.r53.get_paginator = mock.MagicMock()
- self.client.r53.get_paginator().paginate.return_value = [
- {
- "HostedZones": [
- self.EXAMPLE_NET_ZONE,
- self.EXAMPLE_COM_ZONE,
- ]
- }
- ]
-
- result = self.client._find_zone_id_for_domain("foo.example.com")
- assert result == "EXAMPLE"
-
- def test_find_zone_id_for_domain_pagination(self):
- self.client.r53.get_paginator = mock.MagicMock()
- self.client.r53.get_paginator().paginate.return_value = [
- {
- "HostedZones": [
- self.PRIVATE_ZONE,
- self.EXAMPLE_COM_ZONE,
- ]
- },
- {
- "HostedZones": [
- self.PRIVATE_ZONE,
- self.FOO_EXAMPLE_COM_ZONE,
- ]
- }
- ]
-
- result = self.client._find_zone_id_for_domain("foo.example.com")
- assert result == "FOO"
-
- def test_find_zone_id_for_domain_no_results(self):
- self.client.r53.get_paginator = mock.MagicMock()
- self.client.r53.get_paginator().paginate.return_value = []
-
- with pytest.raises(errors.PluginError):
- self.client._find_zone_id_for_domain("foo.example.com")
-
- def test_find_zone_id_for_domain_no_correct_results(self):
- self.client.r53.get_paginator = mock.MagicMock()
- self.client.r53.get_paginator().paginate.return_value = [
- {
- "HostedZones": [
- self.PRIVATE_ZONE,
- self.EXAMPLE_NET_ZONE,
- ]
- },
- ]
-
- with pytest.raises(errors.PluginError):
- self.client._find_zone_id_for_domain("foo.example.com")
-
- def test_change_txt_record(self):
- self.client._find_zone_id_for_domain = mock.MagicMock() # type: ignore
[method-assign, unused-ignore]
- self.client.r53.change_resource_record_sets = mock.MagicMock(
- return_value={"ChangeInfo": {"Id": 1}})
-
- self.client._change_txt_record("FOO", DOMAIN, "foo")
-
- call_count = self.client.r53.change_resource_record_sets.call_count
- assert call_count == 1
-
- def test_change_txt_record_delete(self):
- self.client._find_zone_id_for_domain = mock.MagicMock() # type:
ignore[ method-assign, unused-ignore]
- self.client.r53.change_resource_record_sets = mock.MagicMock(
- return_value={"ChangeInfo": {"Id": 1}})
-
- validation = "some-value"
- validation_record = {"Value": '"{0}"'.format(validation)}
- self.client._resource_records[DOMAIN] = [validation_record]
-
- self.client._change_txt_record("DELETE", DOMAIN, validation)
-
- call_count = self.client.r53.change_resource_record_sets.call_count
- assert call_count == 1
- call_args =
self.client.r53.change_resource_record_sets.call_args_list[0][1]
- call_args_batch = call_args["ChangeBatch"]["Changes"][0]
- assert call_args_batch["Action"] == "DELETE"
- assert call_args_batch["ResourceRecordSet"]["ResourceRecords"] == \
- [validation_record]
-
- def test_change_txt_record_multirecord(self):
- self.client._find_zone_id_for_domain = mock.MagicMock() # type: ignore
[method-assign, unused-ignore]
- self.client._resource_records[DOMAIN] = [
- {"Value": "\"pre-existing-value\""},
- {"Value": "\"pre-existing-value-two\""},
- ]
- self.client.r53.change_resource_record_sets = mock.MagicMock(
- return_value={"ChangeInfo": {"Id": 1}})
-
- self.client._change_txt_record("DELETE", DOMAIN, "pre-existing-value")
-
- call_count = self.client.r53.change_resource_record_sets.call_count
- call_args =
self.client.r53.change_resource_record_sets.call_args_list[0][1]
- call_args_batch = call_args["ChangeBatch"]["Changes"][0]
- assert call_args_batch["Action"] == "UPSERT"
- assert call_args_batch["ResourceRecordSet"]["ResourceRecords"] == \
- [{"Value": "\"pre-existing-value-two\""}]
-
- assert call_count == 1
-
- def test_wait_for_change(self):
- self.client.r53.get_change = mock.MagicMock(
- side_effect=[{"ChangeInfo": {"Status": "PENDING"}},
- {"ChangeInfo": {"Status": "INSYNC"}}])
-
- self.client._wait_for_change("1")
-
- assert self.client.r53.get_change.called
-
-
-if __name__ == "__main__":
- sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/certbot_dns_route53.egg-info/PKG-INFO
new/certbot_dns_route53-4.1.1/certbot_dns_route53.egg-info/PKG-INFO
--- old/certbot_dns_route53-4.0.0/certbot_dns_route53.egg-info/PKG-INFO
2025-04-08 00:03:41.000000000 +0200
+++ new/certbot_dns_route53-4.1.1/certbot_dns_route53.egg-info/PKG-INFO
1970-01-01 01:00:00.000000000 +0100
@@ -1,48 +0,0 @@
-Metadata-Version: 2.4
-Name: certbot-dns-route53
-Version: 4.0.0
-Summary: Route53 DNS Authenticator plugin for Certbot
-Home-page: https://github.com/certbot/certbot
-Author: Certbot Project
-Author-email: [email protected]
-License: Apache License 2.0
-Keywords: certbot,route53,aws
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Environment :: Plugins
-Classifier: Intended Audience :: System Administrators
-Classifier: License :: OSI Approved :: Apache Software License
-Classifier: Operating System :: POSIX :: Linux
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 3
-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.9
-License-File: LICENSE.txt
-Requires-Dist: boto3>=1.15.15
-Requires-Dist: acme>=4.0.0
-Requires-Dist: certbot>=4.0.0
-Provides-Extra: docs
-Requires-Dist: Sphinx>=1.0; extra == "docs"
-Requires-Dist: sphinx_rtd_theme; extra == "docs"
-Provides-Extra: test
-Requires-Dist: pytest; extra == "test"
-Dynamic: author
-Dynamic: author-email
-Dynamic: classifier
-Dynamic: home-page
-Dynamic: keywords
-Dynamic: license
-Dynamic: license-file
-Dynamic: provides-extra
-Dynamic: requires-dist
-Dynamic: requires-python
-Dynamic: summary
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/certbot_dns_route53.egg-info/SOURCES.txt
new/certbot_dns_route53-4.1.1/certbot_dns_route53.egg-info/SOURCES.txt
--- old/certbot_dns_route53-4.0.0/certbot_dns_route53.egg-info/SOURCES.txt
2025-04-08 00:03:41.000000000 +0200
+++ new/certbot_dns_route53-4.1.1/certbot_dns_route53.egg-info/SOURCES.txt
1970-01-01 01:00:00.000000000 +0100
@@ -1,22 +0,0 @@
-LICENSE.txt
-MANIFEST.in
-README.rst
-setup.py
-certbot_dns_route53/__init__.py
-certbot_dns_route53/py.typed
-certbot_dns_route53.egg-info/PKG-INFO
-certbot_dns_route53.egg-info/SOURCES.txt
-certbot_dns_route53.egg-info/dependency_links.txt
-certbot_dns_route53.egg-info/entry_points.txt
-certbot_dns_route53.egg-info/requires.txt
-certbot_dns_route53.egg-info/top_level.txt
-certbot_dns_route53/_internal/__init__.py
-certbot_dns_route53/_internal/dns_route53.py
-certbot_dns_route53/_internal/tests/__init__.py
-certbot_dns_route53/_internal/tests/dns_route53_test.py
-docs/.gitignore
-docs/Makefile
-docs/api.rst
-docs/conf.py
-docs/index.rst
-docs/make.bat
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/certbot_dns_route53.egg-info/dependency_links.txt
new/certbot_dns_route53-4.1.1/certbot_dns_route53.egg-info/dependency_links.txt
---
old/certbot_dns_route53-4.0.0/certbot_dns_route53.egg-info/dependency_links.txt
2025-04-08 00:03:41.000000000 +0200
+++
new/certbot_dns_route53-4.1.1/certbot_dns_route53.egg-info/dependency_links.txt
1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/certbot_dns_route53.egg-info/entry_points.txt
new/certbot_dns_route53-4.1.1/certbot_dns_route53.egg-info/entry_points.txt
--- old/certbot_dns_route53-4.0.0/certbot_dns_route53.egg-info/entry_points.txt
2025-04-08 00:03:41.000000000 +0200
+++ new/certbot_dns_route53-4.1.1/certbot_dns_route53.egg-info/entry_points.txt
1970-01-01 01:00:00.000000000 +0100
@@ -1,3 +0,0 @@
-[certbot.plugins]
-certbot-route53:auth =
certbot_dns_route53._internal.dns_route53:HiddenAuthenticator
-dns-route53 = certbot_dns_route53._internal.dns_route53:Authenticator
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/certbot_dns_route53.egg-info/requires.txt
new/certbot_dns_route53-4.1.1/certbot_dns_route53.egg-info/requires.txt
--- old/certbot_dns_route53-4.0.0/certbot_dns_route53.egg-info/requires.txt
2025-04-08 00:03:41.000000000 +0200
+++ new/certbot_dns_route53-4.1.1/certbot_dns_route53.egg-info/requires.txt
1970-01-01 01:00:00.000000000 +0100
@@ -1,10 +0,0 @@
-boto3>=1.15.15
-acme>=4.0.0
-certbot>=4.0.0
-
-[docs]
-Sphinx>=1.0
-sphinx_rtd_theme
-
-[test]
-pytest
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/certbot_dns_route53.egg-info/top_level.txt
new/certbot_dns_route53-4.1.1/certbot_dns_route53.egg-info/top_level.txt
--- old/certbot_dns_route53-4.0.0/certbot_dns_route53.egg-info/top_level.txt
2025-04-08 00:03:41.000000000 +0200
+++ new/certbot_dns_route53-4.1.1/certbot_dns_route53.egg-info/top_level.txt
1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-certbot_dns_route53
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/certbot_dns_route53-4.0.0/setup.py
new/certbot_dns_route53-4.1.1/setup.py
--- old/certbot_dns_route53-4.0.0/setup.py 2025-04-08 00:03:33.000000000
+0200
+++ new/certbot_dns_route53-4.1.1/setup.py 2025-06-12 20:08:35.000000000
+0200
@@ -4,7 +4,7 @@
from setuptools import find_packages
from setuptools import setup
-version = '4.0.0'
+version = '4.1.1'
install_requires = [
'boto3>=1.15.15',
@@ -38,7 +38,7 @@
author="Certbot Project",
author_email='[email protected]',
license='Apache License 2.0',
- python_requires='>=3.9',
+ python_requires='>=3.9.2',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Plugins',
@@ -59,7 +59,8 @@
'Topic :: System :: Systems Administration',
'Topic :: Utilities',
],
- packages=find_packages(),
+ packages=find_packages(where='src'),
+ package_dir={'': 'src'},
include_package_data=True,
install_requires=install_requires,
keywords=['certbot', 'route53', 'aws'],
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53/__init__.py
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53/__init__.py
--- old/certbot_dns_route53-4.0.0/src/certbot_dns_route53/__init__.py
1970-01-01 01:00:00.000000000 +0100
+++ new/certbot_dns_route53-4.1.1/src/certbot_dns_route53/__init__.py
2025-06-12 20:08:34.000000000 +0200
@@ -0,0 +1,105 @@
+"""
+The `~certbot_dns_route53.dns_route53` plugin automates the process of
+completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
+subsequently removing, TXT records using the Amazon Web Services Route 53 API.
+
+.. note::
+ The plugin is not installed by default. It can be installed by heading to
+ `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_,
choosing your system and
+ selecting the Wildcard tab.
+
+Credentials
+-----------
+Use of this plugin requires a configuration file containing Amazon Web Sevices
+API credentials for an account with the following permissions:
+
+* ``route53:ListHostedZones``
+* ``route53:GetChange``
+* ``route53:ChangeResourceRecordSets``
+
+These permissions can be captured in an AWS policy like the one below. Amazon
+provides `information about managing access
<https://docs.aws.amazon.com/Route53
+/latest/DeveloperGuide/access-control-overview.html>`_ and `information about
+the required permissions <https://docs.aws.amazon.com/Route53/latest
+/DeveloperGuide/r53-api-permissions-ref.html>`_
+
+.. code-block:: json
+ :name: sample-aws-policy.json
+ :caption: Example AWS policy file:
+
+ {
+ "Version": "2012-10-17",
+ "Id": "certbot-dns-route53 sample policy",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "route53:ListHostedZones",
+ "route53:GetChange"
+ ],
+ "Resource": [
+ "*"
+ ]
+ },
+ {
+ "Effect" : "Allow",
+ "Action" : [
+ "route53:ChangeResourceRecordSets"
+ ],
+ "Resource" : [
+ "arn:aws:route53:::hostedzone/YOURHOSTEDZONEID"
+ ]
+ }
+ ]
+ }
+
+The `access keys <https://docs.aws.amazon.com/general/latest/gr
+/aws-sec-cred-types.html#access-keys-and-secret-access-keys>`_ for an account
+with these permissions must be supplied in one of the following ways, which are
+discussed in more detail in the Boto3 library's documentation about
`configuring
+credentials <https://boto3.readthedocs.io/en/latest/guide/configuration.html
+#best-practices-for-configuring-credentials>`_.
+
+* Using the ``AWS_ACCESS_KEY_ID`` and ``AWS_SECRET_ACCESS_KEY`` environment
+ variables.
+* Using a credentials configuration file at the default location,
+ ``~/.aws/credentials``. If you're running on sudo, the credentials
+ will be picked up from the root home.
+* Using a credentials configuration file at a path supplied using the
+ ``AWS_CONFIG_FILE`` environment variable.
+
+.. code-block:: ini
+ :name: config.ini
+ :caption: Example credentials config file:
+
+ [default]
+ aws_access_key_id=AKIAIOSFODNN7EXAMPLE
+ aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
+
+.. caution::
+ You should protect these API credentials as you would a password. Users who
+ can read this file can use these credentials to issue some types of API
calls
+ on your behalf, limited by the permissions assigned to the account. Users
who
+ can cause Certbot to run using these credentials can complete a ``dns-01``
+ challenge to acquire new certificates or revoke existing certificates for
+ domains these credentials are authorized to manage.
+
+
+Examples
+--------
+.. code-block:: bash
+ :caption: To acquire a certificate for ``example.com``
+
+ certbot certonly \\
+ --dns-route53 \\
+ -d example.com
+
+.. code-block:: bash
+ :caption: To acquire a single certificate for both ``example.com`` and
+ ``www.example.com``
+
+ certbot certonly \\
+ --dns-route53 \\
+ -d example.com \\
+ -d www.example.com
+"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53/_internal/__init__.py
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53/_internal/__init__.py
--- old/certbot_dns_route53-4.0.0/src/certbot_dns_route53/_internal/__init__.py
1970-01-01 01:00:00.000000000 +0100
+++ new/certbot_dns_route53-4.1.1/src/certbot_dns_route53/_internal/__init__.py
2025-06-12 20:08:34.000000000 +0200
@@ -0,0 +1 @@
+"""Internal implementation of `~certbot_dns_route53.dns_route53` plugin."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53/_internal/dns_route53.py
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53/_internal/dns_route53.py
---
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53/_internal/dns_route53.py
1970-01-01 01:00:00.000000000 +0100
+++
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53/_internal/dns_route53.py
2025-06-12 20:08:34.000000000 +0200
@@ -0,0 +1,191 @@
+"""Certbot Route53 authenticator plugin."""
+import collections
+import logging
+import time
+from typing import Any
+from typing import Callable
+from typing import DefaultDict
+from typing import Dict
+from typing import Iterable
+from typing import List
+from typing import Type
+from typing import cast
+
+import boto3
+from botocore.exceptions import ClientError
+from botocore.exceptions import NoCredentialsError
+
+from acme import challenges
+from certbot import achallenges
+from certbot import errors
+from certbot import interfaces
+from certbot.achallenges import AnnotatedChallenge
+from certbot.plugins import common
+
+logger = logging.getLogger(__name__)
+
+INSTRUCTIONS = (
+ "To use certbot-dns-route53, configure credentials as described at "
+
"https://boto3.readthedocs.io/en/latest/guide/configuration.html#best-practices-for-configuring-credentials
" # pylint: disable=line-too-long
+ "and add the necessary permissions for Route53 access.")
+
+
+class Authenticator(common.Plugin, interfaces.Authenticator):
+ """Route53 Authenticator
+
+ This authenticator solves a DNS01 challenge by uploading the answer to AWS
+ Route53.
+ """
+
+ description = ("Obtain certificates using a DNS TXT record (if you are
using AWS Route53 for "
+ "DNS).")
+ ttl = 10
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super().__init__(*args, **kwargs)
+ self.r53 = boto3.client("route53")
+ self._attempt_cleanup = False
+ self._resource_records: DefaultDict[str, List[Dict[str, str]]] = \
+ collections.defaultdict(list)
+
+ def more_info(self) -> str:
+ return "Solve a DNS01 challenge using AWS Route53"
+
+ @classmethod
+ def add_parser_arguments(cls, add: Callable[..., None]) -> None:
+ # This authenticator currently adds no extra arguments.
+ pass
+
+ def auth_hint(self, failed_achalls: List[achallenges.AnnotatedChallenge])
-> str:
+ return (
+ 'The Certificate Authority failed to verify the DNS TXT records
created by '
+ '--dns-route53. Ensure the above domains have their DNS hosted by
AWS Route53.'
+ )
+
+ def prepare(self) -> None:
+ pass
+
+ def get_chall_pref(self, unused_domain: str) ->
Iterable[Type[challenges.Challenge]]:
+ return [challenges.DNS01]
+
+ def perform(self, achalls: List[AnnotatedChallenge]) ->
List[challenges.ChallengeResponse]:
+ self._attempt_cleanup = True
+
+ try:
+ change_ids = [
+ self._change_txt_record("UPSERT",
+ achall.validation_domain_name(achall.domain),
+ achall.validation(achall.account_key))
+ for achall in achalls
+ ]
+
+ for change_id in change_ids:
+ self._wait_for_change(change_id)
+ except (NoCredentialsError, ClientError) as e:
+ logger.debug('Encountered error during perform: %s', e,
exc_info=True)
+ raise errors.PluginError("\n".join([str(e), INSTRUCTIONS]))
+ return [achall.response(achall.account_key) for achall in achalls]
+
+ def cleanup(self, achalls: List[achallenges.AnnotatedChallenge]) -> None:
+ if self._attempt_cleanup:
+ for achall in achalls:
+ domain = achall.domain
+ validation_domain_name = achall.validation_domain_name(domain)
+ validation = achall.validation(achall.account_key)
+
+ self._cleanup(validation_domain_name, validation)
+
+ def _cleanup(self, validation_name: str, validation: str) -> None:
+ try:
+ self._change_txt_record("DELETE", validation_name, validation)
+ except (NoCredentialsError, ClientError) as e:
+ logger.debug('Encountered error during cleanup: %s', e,
exc_info=True)
+
+ def _find_zone_id_for_domain(self, domain: str) -> str:
+ """Find the zone id responsible a given FQDN.
+
+ That is, the id for the zone whose name is the longest parent of the
+ domain.
+ """
+ paginator = self.r53.get_paginator("list_hosted_zones")
+ zones: list[tuple[str, str]] = []
+ target_labels = domain.rstrip(".").split(".")
+ for page in paginator.paginate():
+ for zone in page["HostedZones"]:
+ if zone["Config"]["PrivateZone"]:
+ continue
+
+ candidate_labels = zone["Name"].rstrip(".").split(".")
+ if candidate_labels == target_labels[-len(candidate_labels):]:
+ zones.append((zone["Name"], zone["Id"]))
+
+ if not zones:
+ raise errors.PluginError(
+ "Unable to find a Route53 hosted zone for {0}".format(domain)
+ )
+
+ # Order the zones that are suffixes for our desired to domain by
+ # length, this puts them in an order like:
+ # ["foo.bar.baz.com", "bar.baz.com", "baz.com", "com"]
+ # And then we choose the first one, which will be the most specific.
+ zones.sort(key=lambda z: len(z[0]), reverse=True)
+ return zones[0][1]
+
+ def _change_txt_record(self, action: str, validation_domain_name: str,
validation: str) -> str:
+ zone_id = self._find_zone_id_for_domain(validation_domain_name)
+
+ rrecords = self._resource_records[validation_domain_name]
+ challenge = {"Value": '"{0}"'.format(validation)}
+ if action == "DELETE":
+ # Remove the record being deleted from the list of tracked records
+ rrecords.remove(challenge)
+ if rrecords:
+ # Need to update instead, as we're not deleting the rrset
+ action = "UPSERT"
+ else:
+ # Create a new list containing the record to use with DELETE
+ rrecords = [challenge]
+ else:
+ rrecords.append(challenge)
+
+ response = self.r53.change_resource_record_sets(
+ HostedZoneId=zone_id,
+ ChangeBatch={
+ "Comment": "certbot-dns-route53 certificate validation " +
action,
+ "Changes": [
+ {
+ "Action": action,
+ "ResourceRecordSet": {
+ "Name": validation_domain_name,
+ "Type": "TXT",
+ "TTL": self.ttl,
+ "ResourceRecords": rrecords,
+ }
+ }
+ ]
+ }
+ )
+ return cast(str, response["ChangeInfo"]["Id"])
+
+ def _wait_for_change(self, change_id: str) -> None:
+ """Wait for a change to be propagated to all Route53 DNS servers.
+
https://docs.aws.amazon.com/Route53/latest/APIReference/API_GetChange.html
+ """
+ for unused_n in range(0, 120):
+ response = self.r53.get_change(Id=change_id)
+ if response["ChangeInfo"]["Status"] == "INSYNC":
+ return
+ time.sleep(5)
+ raise errors.PluginError(
+ "Timed out waiting for Route53 change. Current status: %s" %
+ response["ChangeInfo"]["Status"])
+
+
+# Our route53 plugin was initially a 3rd party plugin named
`certbot-route53:auth` as described at
+# https://github.com/certbot/certbot/issues/4688. This shim exists to allow
installations using the
+# old plugin name of `certbot-route53:auth` to continue to work without
cluttering things like
+# Certbot's help output with two route53 plugins.
+class HiddenAuthenticator(Authenticator):
+ """A hidden shim around certbot-dns-route53 for backwards compatibility."""
+
+ hidden = True
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53/_internal/tests/__init__.py
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53/_internal/tests/__init__.py
---
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53/_internal/tests/__init__.py
1970-01-01 01:00:00.000000000 +0100
+++
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53/_internal/tests/__init__.py
2025-06-12 20:08:34.000000000 +0200
@@ -0,0 +1 @@
+"""certbot-dns-route53 tests"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53/_internal/tests/dns_route53_test.py
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53/_internal/tests/dns_route53_test.py
---
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53/_internal/tests/dns_route53_test.py
1970-01-01 01:00:00.000000000 +0100
+++
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53/_internal/tests/dns_route53_test.py
2025-06-12 20:08:34.000000000 +0200
@@ -0,0 +1,273 @@
+"""Tests for certbot_dns_route53._internal.dns_route53.Authenticator"""
+
+import sys
+import unittest
+from unittest import mock
+
+from botocore.exceptions import ClientError
+from botocore.exceptions import NoCredentialsError
+import josepy as jose
+import pytest
+
+from acme import challenges
+from certbot import achallenges
+from certbot import errors
+from certbot.compat import os
+from certbot.plugins.dns_test_common import DOMAIN
+from certbot.tests import acme_util
+from certbot.tests import util as test_util
+
+DOMAIN = 'example.com'
+KEY = jose.jwk.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
+
+
+class AuthenticatorTest(unittest.TestCase):
+ # pylint: disable=protected-access
+
+ achall = achallenges.KeyAuthorizationAnnotatedChallenge(
+ challb=acme_util.DNS01, domain=DOMAIN, account_key=KEY)
+
+ def setUp(self):
+ from certbot_dns_route53._internal.dns_route53 import Authenticator
+
+ super().setUp()
+
+ self.config = mock.MagicMock()
+
+ # Set up dummy credentials for testing
+ os.environ["AWS_ACCESS_KEY_ID"] = "dummy_access_key"
+ os.environ["AWS_SECRET_ACCESS_KEY"] = "dummy_secret_access_key"
+
+ self.auth = Authenticator(self.config, "route53")
+
+ def tearDown(self):
+ # Remove the dummy credentials from env vars
+ del os.environ["AWS_ACCESS_KEY_ID"]
+ del os.environ["AWS_SECRET_ACCESS_KEY"]
+
+ def test_more_info(self) -> None:
+ self.assertTrue(isinstance(self.auth.more_info(), str))
+
+ def test_get_chall_pref(self) -> None:
+ self.assertEqual(self.auth.get_chall_pref("example.org"),
[challenges.DNS01])
+
+ def test_perform(self):
+ self.auth._change_txt_record = mock.MagicMock() # type:
ignore[method-assign, unused-ignore]
+ self.auth._wait_for_change = mock.MagicMock() # type: ignore
[method-assign, unused-ignore]
+
+ self.auth.perform([self.achall])
+
+ self.auth._change_txt_record.assert_called_once_with("UPSERT",
+
'_acme-challenge.' + DOMAIN,
+ mock.ANY)
+ assert self.auth._wait_for_change.call_count == 1
+
+ def test_perform_no_credentials_error(self):
+ self.auth._change_txt_record = mock.MagicMock( # type: ignore
[method-assign, unused-ignore]
+ side_effect=NoCredentialsError)
+
+ with pytest.raises(errors.PluginError):
+ self.auth.perform([self.achall])
+
+ def test_perform_client_error(self):
+ self.auth._change_txt_record = mock.MagicMock( # type: ignore
[method-assign, unused-ignore]
+ side_effect=ClientError({"Error": {"Code": "foo"}}, "bar"))
+
+ with pytest.raises(errors.PluginError):
+ self.auth.perform([self.achall])
+
+ def test_cleanup(self):
+ self.auth._attempt_cleanup = True
+
+ self.auth._change_txt_record = mock.MagicMock() # type:
ignore[method-assign, unused-ignore]
+
+ self.auth.cleanup([self.achall])
+
+ self.auth._change_txt_record.assert_called_once_with("DELETE",
+
'_acme-challenge.'+DOMAIN,
+ mock.ANY)
+
+ def test_cleanup_no_credentials_error(self):
+ self.auth._attempt_cleanup = True
+
+ self.auth._change_txt_record = mock.MagicMock( # type: ignore
[method-assign, unused-ignore]
+ side_effect=NoCredentialsError)
+
+ self.auth.cleanup([self.achall])
+
+ def test_cleanup_client_error(self):
+ self.auth._attempt_cleanup = True
+
+ self.auth._change_txt_record = mock.MagicMock( # type: ignore
[method-assign, unused-ignore]
+ side_effect=ClientError({"Error": {"Code": "foo"}}, "bar"))
+
+ self.auth.cleanup([self.achall])
+
+
+class ClientTest(unittest.TestCase):
+ # pylint: disable=protected-access
+
+ PRIVATE_ZONE = {
+ "Id": "BAD-PRIVATE",
+ "Name": "example.com",
+ "Config": {
+ "PrivateZone": True
+ }
+ }
+
+ EXAMPLE_NET_ZONE = {
+ "Id": "BAD-WRONG-TLD",
+ "Name": "example.net",
+ "Config": {
+ "PrivateZone": False
+ }
+ }
+
+ EXAMPLE_COM_ZONE = {
+ "Id": "EXAMPLE",
+ "Name": "example.com",
+ "Config": {
+ "PrivateZone": False
+ }
+ }
+
+ FOO_EXAMPLE_COM_ZONE = {
+ "Id": "FOO",
+ "Name": "foo.example.com",
+ "Config": {
+ "PrivateZone": False
+ }
+ }
+
+ def setUp(self):
+ from certbot_dns_route53._internal.dns_route53 import Authenticator
+
+ self.config = mock.MagicMock()
+
+ # Set up dummy credentials for testing
+ os.environ["AWS_ACCESS_KEY_ID"] = "dummy_access_key"
+ os.environ["AWS_SECRET_ACCESS_KEY"] = "dummy_secret_access_key"
+
+ self.client = Authenticator(self.config, "route53")
+
+ def tearDown(self):
+ # Remove the dummy credentials from env vars
+ del os.environ["AWS_ACCESS_KEY_ID"]
+ del os.environ["AWS_SECRET_ACCESS_KEY"]
+
+ def test_find_zone_id_for_domain(self):
+ self.client.r53.get_paginator = mock.MagicMock()
+ self.client.r53.get_paginator().paginate.return_value = [
+ {
+ "HostedZones": [
+ self.EXAMPLE_NET_ZONE,
+ self.EXAMPLE_COM_ZONE,
+ ]
+ }
+ ]
+
+ result = self.client._find_zone_id_for_domain("foo.example.com")
+ assert result == "EXAMPLE"
+
+ def test_find_zone_id_for_domain_pagination(self):
+ self.client.r53.get_paginator = mock.MagicMock()
+ self.client.r53.get_paginator().paginate.return_value = [
+ {
+ "HostedZones": [
+ self.PRIVATE_ZONE,
+ self.EXAMPLE_COM_ZONE,
+ ]
+ },
+ {
+ "HostedZones": [
+ self.PRIVATE_ZONE,
+ self.FOO_EXAMPLE_COM_ZONE,
+ ]
+ }
+ ]
+
+ result = self.client._find_zone_id_for_domain("foo.example.com")
+ assert result == "FOO"
+
+ def test_find_zone_id_for_domain_no_results(self):
+ self.client.r53.get_paginator = mock.MagicMock()
+ self.client.r53.get_paginator().paginate.return_value = []
+
+ with pytest.raises(errors.PluginError):
+ self.client._find_zone_id_for_domain("foo.example.com")
+
+ def test_find_zone_id_for_domain_no_correct_results(self):
+ self.client.r53.get_paginator = mock.MagicMock()
+ self.client.r53.get_paginator().paginate.return_value = [
+ {
+ "HostedZones": [
+ self.PRIVATE_ZONE,
+ self.EXAMPLE_NET_ZONE,
+ ]
+ },
+ ]
+
+ with pytest.raises(errors.PluginError):
+ self.client._find_zone_id_for_domain("foo.example.com")
+
+ def test_change_txt_record(self):
+ self.client._find_zone_id_for_domain = mock.MagicMock() # type: ignore
[method-assign, unused-ignore]
+ self.client.r53.change_resource_record_sets = mock.MagicMock(
+ return_value={"ChangeInfo": {"Id": 1}})
+
+ self.client._change_txt_record("FOO", DOMAIN, "foo")
+
+ call_count = self.client.r53.change_resource_record_sets.call_count
+ assert call_count == 1
+
+ def test_change_txt_record_delete(self):
+ self.client._find_zone_id_for_domain = mock.MagicMock() # type:
ignore[ method-assign, unused-ignore]
+ self.client.r53.change_resource_record_sets = mock.MagicMock(
+ return_value={"ChangeInfo": {"Id": 1}})
+
+ validation = "some-value"
+ validation_record = {"Value": '"{0}"'.format(validation)}
+ self.client._resource_records[DOMAIN] = [validation_record]
+
+ self.client._change_txt_record("DELETE", DOMAIN, validation)
+
+ call_count = self.client.r53.change_resource_record_sets.call_count
+ assert call_count == 1
+ call_args =
self.client.r53.change_resource_record_sets.call_args_list[0][1]
+ call_args_batch = call_args["ChangeBatch"]["Changes"][0]
+ assert call_args_batch["Action"] == "DELETE"
+ assert call_args_batch["ResourceRecordSet"]["ResourceRecords"] == \
+ [validation_record]
+
+ def test_change_txt_record_multirecord(self):
+ self.client._find_zone_id_for_domain = mock.MagicMock() # type: ignore
[method-assign, unused-ignore]
+ self.client._resource_records[DOMAIN] = [
+ {"Value": "\"pre-existing-value\""},
+ {"Value": "\"pre-existing-value-two\""},
+ ]
+ self.client.r53.change_resource_record_sets = mock.MagicMock(
+ return_value={"ChangeInfo": {"Id": 1}})
+
+ self.client._change_txt_record("DELETE", DOMAIN, "pre-existing-value")
+
+ call_count = self.client.r53.change_resource_record_sets.call_count
+ call_args =
self.client.r53.change_resource_record_sets.call_args_list[0][1]
+ call_args_batch = call_args["ChangeBatch"]["Changes"][0]
+ assert call_args_batch["Action"] == "UPSERT"
+ assert call_args_batch["ResourceRecordSet"]["ResourceRecords"] == \
+ [{"Value": "\"pre-existing-value-two\""}]
+
+ assert call_count == 1
+
+ def test_wait_for_change(self):
+ self.client.r53.get_change = mock.MagicMock(
+ side_effect=[{"ChangeInfo": {"Status": "PENDING"}},
+ {"ChangeInfo": {"Status": "INSYNC"}}])
+
+ self.client._wait_for_change("1")
+
+ assert self.client.r53.get_change.called
+
+
+if __name__ == "__main__":
+ sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53.egg-info/PKG-INFO
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53.egg-info/PKG-INFO
--- old/certbot_dns_route53-4.0.0/src/certbot_dns_route53.egg-info/PKG-INFO
1970-01-01 01:00:00.000000000 +0100
+++ new/certbot_dns_route53-4.1.1/src/certbot_dns_route53.egg-info/PKG-INFO
2025-06-12 20:08:43.000000000 +0200
@@ -0,0 +1,48 @@
+Metadata-Version: 2.4
+Name: certbot-dns-route53
+Version: 4.1.1
+Summary: Route53 DNS Authenticator plugin for Certbot
+Home-page: https://github.com/certbot/certbot
+Author: Certbot Project
+Author-email: [email protected]
+License: Apache License 2.0
+Keywords: certbot,route53,aws
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Plugins
+Classifier: Intended Audience :: System Administrators
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Operating System :: POSIX :: Linux
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+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.9.2
+License-File: LICENSE.txt
+Requires-Dist: boto3>=1.15.15
+Requires-Dist: acme>=4.1.1
+Requires-Dist: certbot>=4.1.1
+Provides-Extra: docs
+Requires-Dist: Sphinx>=1.0; extra == "docs"
+Requires-Dist: sphinx_rtd_theme; extra == "docs"
+Provides-Extra: test
+Requires-Dist: pytest; extra == "test"
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: home-page
+Dynamic: keywords
+Dynamic: license
+Dynamic: license-file
+Dynamic: provides-extra
+Dynamic: requires-dist
+Dynamic: requires-python
+Dynamic: summary
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53.egg-info/SOURCES.txt
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53.egg-info/SOURCES.txt
--- old/certbot_dns_route53-4.0.0/src/certbot_dns_route53.egg-info/SOURCES.txt
1970-01-01 01:00:00.000000000 +0100
+++ new/certbot_dns_route53-4.1.1/src/certbot_dns_route53.egg-info/SOURCES.txt
2025-06-12 20:08:43.000000000 +0200
@@ -0,0 +1,22 @@
+LICENSE.txt
+MANIFEST.in
+README.rst
+setup.py
+docs/.gitignore
+docs/Makefile
+docs/api.rst
+docs/conf.py
+docs/index.rst
+docs/make.bat
+src/certbot_dns_route53/__init__.py
+src/certbot_dns_route53/py.typed
+src/certbot_dns_route53.egg-info/PKG-INFO
+src/certbot_dns_route53.egg-info/SOURCES.txt
+src/certbot_dns_route53.egg-info/dependency_links.txt
+src/certbot_dns_route53.egg-info/entry_points.txt
+src/certbot_dns_route53.egg-info/requires.txt
+src/certbot_dns_route53.egg-info/top_level.txt
+src/certbot_dns_route53/_internal/__init__.py
+src/certbot_dns_route53/_internal/dns_route53.py
+src/certbot_dns_route53/_internal/tests/__init__.py
+src/certbot_dns_route53/_internal/tests/dns_route53_test.py
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53.egg-info/dependency_links.txt
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53.egg-info/dependency_links.txt
---
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53.egg-info/dependency_links.txt
1970-01-01 01:00:00.000000000 +0100
+++
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53.egg-info/dependency_links.txt
2025-06-12 20:08:43.000000000 +0200
@@ -0,0 +1 @@
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53.egg-info/entry_points.txt
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53.egg-info/entry_points.txt
---
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53.egg-info/entry_points.txt
1970-01-01 01:00:00.000000000 +0100
+++
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53.egg-info/entry_points.txt
2025-06-12 20:08:43.000000000 +0200
@@ -0,0 +1,3 @@
+[certbot.plugins]
+certbot-route53:auth =
certbot_dns_route53._internal.dns_route53:HiddenAuthenticator
+dns-route53 = certbot_dns_route53._internal.dns_route53:Authenticator
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53.egg-info/requires.txt
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53.egg-info/requires.txt
--- old/certbot_dns_route53-4.0.0/src/certbot_dns_route53.egg-info/requires.txt
1970-01-01 01:00:00.000000000 +0100
+++ new/certbot_dns_route53-4.1.1/src/certbot_dns_route53.egg-info/requires.txt
2025-06-12 20:08:43.000000000 +0200
@@ -0,0 +1,10 @@
+boto3>=1.15.15
+acme>=4.1.1
+certbot>=4.1.1
+
+[docs]
+Sphinx>=1.0
+sphinx_rtd_theme
+
+[test]
+pytest
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53.egg-info/top_level.txt
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53.egg-info/top_level.txt
---
old/certbot_dns_route53-4.0.0/src/certbot_dns_route53.egg-info/top_level.txt
1970-01-01 01:00:00.000000000 +0100
+++
new/certbot_dns_route53-4.1.1/src/certbot_dns_route53.egg-info/top_level.txt
2025-06-12 20:08:43.000000000 +0200
@@ -0,0 +1 @@
+certbot_dns_route53