From 24eb299a9b30e32c9041b22c4d1688ec8032c2bf Mon Sep 17 00:00:00 2001 From: Jeremy Gillula Date: Tue, 16 Apr 2019 16:27:22 -0700 Subject: [PATCH 1/5] Added a link to the EFF Public Projects Code of Conduct to the readme. --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index f55581268..0f26901d7 100644 --- a/README.rst +++ b/README.rst @@ -28,6 +28,8 @@ Contributing If you'd like to contribute to this project please read `Developer Guide `_. +This project is governed by `EFF's Public Projects Code of Conduct `_. + .. _installation: Installation From 7e5dcaa38397438969eb46f1fc8c04046afa7261 Mon Sep 17 00:00:00 2001 From: Jeremy Gillula Date: Tue, 16 Apr 2019 16:28:32 -0700 Subject: [PATCH 2/5] Adding the EFF Public Projects Code of Conduct to the contributing guide --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d740b7d89..07187eb59 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,3 +33,5 @@ started. In particular, we recommend you read these sections - [Finding issues to work on](https://certbot.eff.org/docs/contributing.html#find-issues-to-work-on) - [Coding style](https://certbot.eff.org/docs/contributing.html#coding-style) - [Submitting a pull request](https://certbot.eff.org/docs/contributing.html#submitting-a-pull-request) + - [EFF's Public Projects Code of Conduct](https://www.eff.org/pages/eppcode) + From f5d0d4241f790fa5890e100637bba9def0448ba2 Mon Sep 17 00:00:00 2001 From: Jeremy Gillula Date: Wed, 17 Apr 2019 11:36:26 -0700 Subject: [PATCH 3/5] Added a CODE_OF_CONDUCT.md file so Github doesn't complain --- CODE_OF_CONDUCT.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..142b31c93 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1 @@ +This project is governed by [EFF's Public Projects Code of Conduct](https://www.eff.org/pages/eppcode). \ No newline at end of file From 9dd2990e5939edf891831439d62907d1769dac34 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Wed, 24 Apr 2019 00:10:15 +0200 Subject: [PATCH 4/5] Remove keyAuthorization fallback dump in challenges response (#6975) Fixes #6974. This PR removes the fallback that consists in retrying to send the keyAuthorization field during a challenge request in case of malformed request. * Remove keyAuthorization fallback dump in challenges response * Correct import * Add changelog entry --- CHANGELOG.md | 4 ++++ acme/acme/challenges.py | 17 +---------------- acme/acme/challenges_test.py | 8 -------- acme/acme/client.py | 19 +------------------ acme/acme/client_test.py | 28 ---------------------------- 5 files changed, 6 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac27aa772..afc8b8b21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). * Updated Certbot and its plugins to improve the handling of file system permissions on Windows as a step towards adding proper Windows support to Certbot. * Updated urllib3 to 1.24.2 in certbot-auto. +* Removed the fallback introduced with 0.32.0 in `acme` to retry a challenge response + with a `keyAuthorization` if sending the response without this field caused a + `malformed` error to be received from the ACME server. ### Fixed @@ -26,6 +29,7 @@ Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was: +* acme * certbot * certbot-apache * certbot-dns-cloudflare diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 01298d46f..78991608a 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -107,10 +107,6 @@ class KeyAuthorizationChallengeResponse(ChallengeResponse): key_authorization = jose.Field("keyAuthorization") thumbprint_hash_function = hashes.SHA256 - def __init__(self, *args, **kwargs): - super(KeyAuthorizationChallengeResponse, self).__init__(*args, **kwargs) - self._dump_authorization_key(False) - def verify(self, chall, account_public_key): """Verify the key authorization. @@ -143,20 +139,9 @@ class KeyAuthorizationChallengeResponse(ChallengeResponse): return True - def _dump_authorization_key(self, dump): - # type: (bool) -> None - """ - Set if keyAuthorization is dumped in the JSON representation of this ChallengeResponse. - NB: This method is declared as private because it will eventually be removed. - :param bool dump: True to dump the keyAuthorization, False otherwise - """ - object.__setattr__(self, '_dump_auth_key', dump) - def to_partial_json(self): jobj = super(KeyAuthorizationChallengeResponse, self).to_partial_json() - if not self._dump_auth_key: # pylint: disable=no-member - jobj.pop('keyAuthorization', None) - + jobj.pop('keyAuthorization', None) return jobj diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index f5f914005..9d3a92fa5 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -95,8 +95,6 @@ class DNS01ResponseTest(unittest.TestCase): def test_to_partial_json(self): self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'}, self.msg.to_partial_json()) - self.msg._dump_authorization_key(True) # pylint: disable=protected-access - self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import DNS01Response @@ -169,8 +167,6 @@ class HTTP01ResponseTest(unittest.TestCase): def test_to_partial_json(self): self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'}, self.msg.to_partial_json()) - self.msg._dump_authorization_key(True) # pylint: disable=protected-access - self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import HTTP01Response @@ -292,8 +288,6 @@ class TLSSNI01ResponseTest(unittest.TestCase): def test_to_partial_json(self): self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'}, self.response.to_partial_json()) - self.response._dump_authorization_key(True) # pylint: disable=protected-access - self.assertEqual(self.jmsg, self.response.to_partial_json()) def test_from_json(self): from acme.challenges import TLSSNI01Response @@ -428,8 +422,6 @@ class TLSALPN01ResponseTest(unittest.TestCase): def test_to_partial_json(self): self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'}, self.msg.to_partial_json()) - self.msg._dump_authorization_key(True) # pylint: disable=protected-access - self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import TLSALPN01Response diff --git a/acme/acme/client.py b/acme/acme/client.py index 5cad0acbe..a41787756 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -17,7 +17,6 @@ import requests from requests.adapters import HTTPAdapter from requests_toolbelt.adapters.source import SourceAddressAdapter -from acme import challenges from acme import crypto_util from acme import errors from acme import jws @@ -156,23 +155,7 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes :raises .UnexpectedUpdate: """ - # Because sending keyAuthorization in a response challenge has been removed from the ACME - # spec, it is not included in the KeyAuthorizationResponseChallenge JSON by default. - # However as a migration path, we temporarily expect a malformed error from the server, - # and fallback by resending the challenge response with the keyAuthorization field. - # TODO: Remove this fallback for Certbot 0.34.0 - try: - response = self._post(challb.uri, response) - except messages.Error as error: - if (error.code == 'malformed' - and isinstance(response, challenges.KeyAuthorizationChallengeResponse)): - logger.debug('Error while responding to a challenge without keyAuthorization ' - 'in the JWS, your ACME CA server may not support it:\n%s', error) - logger.debug('Retrying request with keyAuthorization set.') - response._dump_authorization_key(True) # pylint: disable=protected-access - response = self._post(challb.uri, response) - else: - raise + response = self._post(challb.uri, response) try: authzr_uri = response.links['up']['url'] except KeyError: diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 5b2703701..406201751 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -461,34 +461,6 @@ class ClientTest(ClientTestBase): errors.ClientError, self.client.answer_challenge, self.challr.body, challenges.DNSResponse(validation=None)) - def test_answer_challenge_key_authorization_fallback(self): - self.response.links['up'] = {'url': self.challr.authzr_uri} - self.response.json.return_value = self.challr.body.to_json() - - def _wrapper_post(url, obj, *args, **kwargs): # pylint: disable=unused-argument - """ - Simulate an old ACME CA server, that would respond a 'malformed' - error if keyAuthorization is missing. - """ - jobj = obj.to_partial_json() - if 'keyAuthorization' not in jobj: - raise messages.Error.with_code('malformed') - return self.response - self.net.post.side_effect = _wrapper_post - - # This challenge response is of type KeyAuthorizationChallengeResponse, so the fallback - # should be triggered, and avoid an exception. - http_chall_response = challenges.HTTP01Response(key_authorization='test', - resource=mock.MagicMock()) - self.client.answer_challenge(self.challr.body, http_chall_response) - - # This challenge response is not of type KeyAuthorizationChallengeResponse, so the fallback - # should not be triggered, leading to an exception. - dns_chall_response = challenges.DNSResponse(validation=None) - self.assertRaises( - errors.Error, self.client.answer_challenge, - self.challr.body, dns_chall_response) - def test_retry_after_date(self): self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT' self.assertEqual( From 333ea90d1b1348933aa6e586472bb62e182bfebc Mon Sep 17 00:00:00 2001 From: Trinopoty Biswas Date: Wed, 24 Apr 2019 20:41:42 +0000 Subject: [PATCH 5/5] Added support for linode version 4 tokens (#6588) * certbot-dns-linode : Added support for linode version 4 tokens * certbot-dns-linode : Added credentials ini option to override automatic api version detection * certbot-dns-linode : Added clearer messages and documentation based on review * certbot-dns-linode : Added check for empty 'linode_version' config instead of missing * certbot-dns-linode : Fix rebase on master * certbot-dns-linode : Updated local-oldest-requirements.txt * Updated CHANGELOG to indicate Linode v4 API key support --- CHANGELOG.md | 2 + .../certbot_dns_linode/__init__.py | 4 +- .../certbot_dns_linode/dns_linode.py | 46 ++++++-- .../certbot_dns_linode/dns_linode_test.py | 103 +++++++++++++++++- .../local-oldest-requirements.txt | 1 + certbot-dns-linode/setup.py | 2 +- 6 files changed, 146 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afc8b8b21..c1af9ffb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). * Removed the fallback introduced with 0.32.0 in `acme` to retry a challenge response with a `keyAuthorization` if sending the response without this field caused a `malformed` error to be received from the ACME server. +* Linode DNS plugin now supports api keys created from their new panel + at [cloud.linode.com](https://cloud.linode.com) ### Fixed diff --git a/certbot-dns-linode/certbot_dns_linode/__init__.py b/certbot-dns-linode/certbot_dns_linode/__init__.py index 0a6ccec61..107781a13 100644 --- a/certbot-dns-linode/certbot_dns_linode/__init__.py +++ b/certbot-dns-linode/certbot_dns_linode/__init__.py @@ -27,7 +27,8 @@ Credentials Use of this plugin requires a configuration file containing Linode API credentials, obtained from your Linode account's `Applications & API -Tokens page `_. +Tokens page (legacy) `_ or `Applications +& API Tokens page (new) `_. .. code-block:: ini :name: credentials.ini @@ -35,6 +36,7 @@ Tokens page `_. # Linode API credentials used by Certbot dns_linode_key = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ64 + dns_linode_version = [|3|4] The path to this file can be provided interactively or using the ``--dns-linode-credentials`` command-line argument. Certbot records the path diff --git a/certbot-dns-linode/certbot_dns_linode/dns_linode.py b/certbot-dns-linode/certbot_dns_linode/dns_linode.py index c2097a7d6..507ad5e53 100644 --- a/certbot-dns-linode/certbot_dns_linode/dns_linode.py +++ b/certbot-dns-linode/certbot_dns_linode/dns_linode.py @@ -1,8 +1,10 @@ """DNS Authenticator for Linode.""" import logging +import re import zope.interface from lexicon.providers import linode +from lexicon.providers import linode4 from certbot import errors from certbot import interfaces @@ -12,6 +14,7 @@ from certbot.plugins import dns_common_lexicon logger = logging.getLogger(__name__) API_KEY_URL = 'https://manager.linode.com/profile/api' +API_KEY_URL_V4 = 'https://cloud.linode.com/profile/tokens' @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) @@ -41,7 +44,8 @@ class Authenticator(dns_common.DNSAuthenticator): 'credentials', 'Linode credentials INI file', { - 'key': 'API key for Linode account, obtained from {0}'.format(API_KEY_URL) + 'key': 'API key for Linode account, obtained from {0} or {1}' + .format(API_KEY_URL, API_KEY_URL_V4) } ) @@ -52,7 +56,23 @@ class Authenticator(dns_common.DNSAuthenticator): self._get_linode_client().del_txt_record(domain, validation_name, validation) def _get_linode_client(self): - return _LinodeLexiconClient(self.credentials.conf('key')) + api_key = self.credentials.conf('key') + api_version = self.credentials.conf('version') + if api_version == '': + api_version = None + + if not api_version: + api_version = 3 + + # Match for v4 api key + regex_v4 = re.compile('^[0-9a-f]{64}$') + regex_match = regex_v4.match(api_key) + if regex_match: + api_version = 4 + else: + api_version = int(api_version) + + return _LinodeLexiconClient(api_key, api_version) class _LinodeLexiconClient(dns_common_lexicon.LexiconClient): @@ -60,14 +80,26 @@ class _LinodeLexiconClient(dns_common_lexicon.LexiconClient): Encapsulates all communication with the Linode API. """ - def __init__(self, api_key): + def __init__(self, api_key, api_version): super(_LinodeLexiconClient, self).__init__() - config = dns_common_lexicon.build_lexicon_config('linode', {}, { - 'auth_token': api_key, - }) + self.api_version = api_version - self.provider = linode.Provider(config) + if api_version == 3: + config = dns_common_lexicon.build_lexicon_config('linode', {}, { + 'auth_token': api_key, + }) + + self.provider = linode.Provider(config) + elif api_version == 4: + config = dns_common_lexicon.build_lexicon_config('linode4', {}, { + 'auth_token': api_key, + }) + + self.provider = linode4.Provider(config) + else: + raise errors.PluginError('Invalid api version specified: {0}. (Supported: 3, 4)' + .format(api_version)) def _handle_general_error(self, e, domain_name): if not str(e).startswith('Domain not found'): diff --git a/certbot-dns-linode/certbot_dns_linode/dns_linode_test.py b/certbot-dns-linode/certbot_dns_linode/dns_linode_test.py index c1a4e0ec0..153f8b51d 100644 --- a/certbot-dns-linode/certbot_dns_linode/dns_linode_test.py +++ b/certbot-dns-linode/certbot_dns_linode/dns_linode_test.py @@ -4,12 +4,16 @@ import unittest import mock +from certbot import errors from certbot.compat import os from certbot.plugins import dns_test_common from certbot.plugins import dns_test_common_lexicon from certbot.tests import util as test_util +from certbot_dns_linode.dns_linode import Authenticator TOKEN = 'a-token' +TOKEN_V3 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ64' +TOKEN_V4 = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common_lexicon.BaseLexiconAuthenticatorTest): @@ -17,8 +21,6 @@ class AuthenticatorTest(test_util.TempDirTestCase, def setUp(self): super(AuthenticatorTest, self).setUp() - from certbot_dns_linode.dns_linode import Authenticator - path = os.path.join(self.tempdir, 'file.ini') dns_test_common.write({"linode_key": TOKEN}, path) @@ -31,6 +33,89 @@ class AuthenticatorTest(test_util.TempDirTestCase, # _get_linode_client | pylint: disable=protected-access self.auth._get_linode_client = mock.MagicMock(return_value=self.mock_client) + # pylint: disable=protected-access + def test_api_version_3_detection(self): + path = os.path.join(self.tempdir, 'file_3_auto.ini') + dns_test_common.write({"linode_key": TOKEN_V3}, path) + + config = mock.MagicMock(linode_credentials=path, + linode_propagation_seconds=0) + auth = Authenticator(config, "linode") + auth._setup_credentials() + client = auth._get_linode_client() + self.assertEqual(3, client.api_version) + + # pylint: disable=protected-access + def test_api_version_4_detection(self): + path = os.path.join(self.tempdir, 'file_4_auto.ini') + dns_test_common.write({"linode_key": TOKEN_V4}, path) + + config = mock.MagicMock(linode_credentials=path, + linode_propagation_seconds=0) + auth = Authenticator(config, "linode") + auth._setup_credentials() + client = auth._get_linode_client() + self.assertEqual(4, client.api_version) + + # pylint: disable=protected-access + def test_api_version_3_detection_empty_version(self): + path = os.path.join(self.tempdir, 'file_3_auto_empty.ini') + dns_test_common.write({"linode_key": TOKEN_V3, "linode_version": ""}, path) + + config = mock.MagicMock(linode_credentials=path, + linode_propagation_seconds=0) + auth = Authenticator(config, "linode") + auth._setup_credentials() + client = auth._get_linode_client() + self.assertEqual(3, client.api_version) + + # pylint: disable=protected-access + def test_api_version_4_detection_empty_version(self): + path = os.path.join(self.tempdir, 'file_4_auto_empty.ini') + dns_test_common.write({"linode_key": TOKEN_V4, "linode_version": ""}, path) + + config = mock.MagicMock(linode_credentials=path, + linode_propagation_seconds=0) + auth = Authenticator(config, "linode") + auth._setup_credentials() + client = auth._get_linode_client() + self.assertEqual(4, client.api_version) + + # pylint: disable=protected-access + def test_api_version_3_manual(self): + path = os.path.join(self.tempdir, 'file_3_manual.ini') + dns_test_common.write({"linode_key": TOKEN_V4, "linode_version": 3}, path) + + config = mock.MagicMock(linode_credentials=path, + linode_propagation_seconds=0) + auth = Authenticator(config, "linode") + auth._setup_credentials() + client = auth._get_linode_client() + self.assertEqual(3, client.api_version) + + # pylint: disable=protected-access + def test_api_version_4_manual(self): + path = os.path.join(self.tempdir, 'file_4_manual.ini') + dns_test_common.write({"linode_key": TOKEN_V3, "linode_version": 4}, path) + + config = mock.MagicMock(linode_credentials=path, + linode_propagation_seconds=0) + auth = Authenticator(config, "linode") + auth._setup_credentials() + client = auth._get_linode_client() + self.assertEqual(4, client.api_version) + + # pylint: disable=protected-access + def test_api_version_error(self): + path = os.path.join(self.tempdir, 'file_version_error.ini') + dns_test_common.write({"linode_key": TOKEN_V3, "linode_version": 5}, path) + + config = mock.MagicMock(linode_credentials=path, + linode_propagation_seconds=0) + auth = Authenticator(config, "linode") + auth._setup_credentials() + self.assertRaises(errors.PluginError, auth._get_linode_client) + class LinodeLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest): DOMAIN_NOT_FOUND = Exception('Domain not found') @@ -38,7 +123,19 @@ class LinodeLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLex def setUp(self): from certbot_dns_linode.dns_linode import _LinodeLexiconClient - self.client = _LinodeLexiconClient(TOKEN) + self.client = _LinodeLexiconClient(TOKEN, 3) + + self.provider_mock = mock.MagicMock() + self.client.provider = self.provider_mock + +class Linode4LexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest): + + DOMAIN_NOT_FOUND = Exception('Domain not found') + + def setUp(self): + from certbot_dns_linode.dns_linode import _LinodeLexiconClient + + self.client = _LinodeLexiconClient(TOKEN, 4) self.provider_mock = mock.MagicMock() self.client.provider = self.provider_mock diff --git a/certbot-dns-linode/local-oldest-requirements.txt b/certbot-dns-linode/local-oldest-requirements.txt index 2b3ba9f32..d48a789bb 100644 --- a/certbot-dns-linode/local-oldest-requirements.txt +++ b/certbot-dns-linode/local-oldest-requirements.txt @@ -1,3 +1,4 @@ # Remember to update setup.py to match the package versions below. acme[dev]==0.31.0 -e .[dev] +dns-lexicon==2.2.3 diff --git a/certbot-dns-linode/setup.py b/certbot-dns-linode/setup.py index e43ab8de9..771e09381 100644 --- a/certbot-dns-linode/setup.py +++ b/certbot-dns-linode/setup.py @@ -7,7 +7,7 @@ version = '0.34.0.dev0' install_requires = [ 'acme>=0.31.0', 'certbot>=0.34.0.dev0', - 'dns-lexicon>=2.2.1', + 'dns-lexicon>=2.2.3', 'mock', 'setuptools', 'zope.interface',