Merge branch 'master' into certbot-ci-part5

This commit is contained in:
Adrien Ferrand 2019-04-25 16:28:02 +02:00
commit dcad58f4ac
13 changed files with 157 additions and 82 deletions

View file

@ -17,6 +17,11 @@ 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.
* Linode DNS plugin now supports api keys created from their new panel
at [cloud.linode.com](https://cloud.linode.com)
### Fixed
@ -26,6 +31,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

1
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1 @@
This project is governed by [EFF's Public Projects Code of Conduct](https://www.eff.org/pages/eppcode).

View file

@ -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)

View file

@ -28,6 +28,8 @@ Contributing
If you'd like to contribute to this project please read `Developer Guide
<https://certbot.eff.org/docs/contributing.html>`_.
This project is governed by `EFF's Public Projects Code of Conduct <https://www.eff.org/pages/eppcode>`_.
.. _installation:
How to run the client

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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(

View file

@ -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 <https://manager.linode.com/profile/api>`_.
Tokens page (legacy) <https://manager.linode.com/profile/api>`_ or `Applications
& API Tokens page (new) <https://cloud.linode.com/profile/tokens>`_.
.. code-block:: ini
:name: credentials.ini
@ -35,6 +36,7 @@ Tokens page <https://manager.linode.com/profile/api>`_.
# Linode API credentials used by Certbot
dns_linode_key = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ64
dns_linode_version = [<blank>|3|4]
The path to this file can be provided interactively or using the
``--dns-linode-credentials`` command-line argument. Certbot records the path

View file

@ -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'):

View file

@ -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

View file

@ -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

View file

@ -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',