mirror of
https://github.com/certbot/certbot.git
synced 2026-06-13 18:50:20 -04:00
Merge branch 'master' into certbot-ci-part5
This commit is contained in:
commit
dcad58f4ac
13 changed files with 157 additions and 82 deletions
|
|
@ -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
1
CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
This project is governed by [EFF's Public Projects Code of Conduct](https://www.eff.org/pages/eppcode).
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue