mirror of
https://github.com/certbot/certbot.git
synced 2026-08-01 19:02:52 +02:00
Merge branch 'test-add_dns01_challenge' into add_dns01_challenge
This commit is contained in:
+7
-20
@@ -6,8 +6,6 @@ import logging
|
|||||||
import socket
|
import socket
|
||||||
|
|
||||||
from cryptography.hazmat.primitives import hashes
|
from cryptography.hazmat.primitives import hashes
|
||||||
import dns.resolver
|
|
||||||
import dns.exception
|
|
||||||
import OpenSSL
|
import OpenSSL
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@@ -17,7 +15,6 @@ from acme import fields
|
|||||||
from acme import jose
|
from acme import jose
|
||||||
from acme import other
|
from acme import other
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -244,7 +241,13 @@ class DNS01Response(KeyAuthorizationChallengeResponse):
|
|||||||
validation = chall.validation(account_public_key)
|
validation = chall.validation(account_public_key)
|
||||||
logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name)
|
logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name)
|
||||||
|
|
||||||
txt_records = txt_records_for_name(validation_domain_name)
|
try:
|
||||||
|
from acme import dns_resolver
|
||||||
|
txt_records = dns_resolver.txt_records_for_name(
|
||||||
|
validation_domain_name)
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError("Local validation for 'dns-01' challenges "
|
||||||
|
"requires 'dnspython'")
|
||||||
exists = validation in txt_records
|
exists = validation in txt_records
|
||||||
if not exists:
|
if not exists:
|
||||||
logger.debug("Key authorization from response (%r) doesn't match "
|
logger.debug("Key authorization from response (%r) doesn't match "
|
||||||
@@ -703,19 +706,3 @@ class DNSResponse(ChallengeResponse):
|
|||||||
"""
|
"""
|
||||||
return chall.check_validation(self.validation, account_public_key)
|
return chall.check_validation(self.validation, account_public_key)
|
||||||
|
|
||||||
|
|
||||||
def txt_records_for_name(name):
|
|
||||||
"""Resolve the name and return the TXT records.
|
|
||||||
|
|
||||||
:param unicode name: Domain name being verified.
|
|
||||||
|
|
||||||
:returns: A list of txt records, if empty the name could not be resolved
|
|
||||||
:rtype: list of unicode
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
dns_response = dns.resolver.query(name, 'TXT')
|
|
||||||
except dns.exception.DNSException as error:
|
|
||||||
logger.error("Unable to resolve %s: %s", name, error)
|
|
||||||
return []
|
|
||||||
return [txt_rec for rdata in dns_response for txt_rec in rdata.strings]
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Tests for acme.challenges."""
|
"""Tests for acme.challenges."""
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import dns.rrset
|
|
||||||
import mock
|
import mock
|
||||||
import OpenSSL
|
import OpenSSL
|
||||||
import requests
|
import requests
|
||||||
@@ -94,16 +93,6 @@ class DNS01ResponseTest(unittest.TestCase):
|
|||||||
self.chall = DNS01(token=(b'x' * 16))
|
self.chall = DNS01(token=(b'x' * 16))
|
||||||
self.response = self.chall.response(KEY)
|
self.response = self.chall.response(KEY)
|
||||||
|
|
||||||
def create_txt_response(self, name, txt_records):
|
|
||||||
"""
|
|
||||||
Returns an RRSet containing the 'txt_records' as the result of a DNS
|
|
||||||
query for 'name'.
|
|
||||||
|
|
||||||
This takes advantage of the fact that an Answer object mostly behaves
|
|
||||||
like an RRset.
|
|
||||||
"""
|
|
||||||
return dns.rrset.from_text_list(name, 60, "IN", "TXT", txt_records)
|
|
||||||
|
|
||||||
def test_to_partial_json(self):
|
def test_to_partial_json(self):
|
||||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||||
|
|
||||||
@@ -119,36 +108,25 @@ class DNS01ResponseTest(unittest.TestCase):
|
|||||||
key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem'))
|
key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem'))
|
||||||
self.response.simple_verify(self.chall, "local", key2.public_key())
|
self.response.simple_verify(self.chall, "local", key2.public_key())
|
||||||
|
|
||||||
@mock.patch("acme.challenges.dns.resolver.query")
|
@mock.patch("acme.dns_resolver.txt_records_for_name")
|
||||||
def test_simple_verify_good_validation(self, mock_dns):
|
def test_simple_verify_good_validation(self, mock_resolver):
|
||||||
mock_dns.return_value = self.create_txt_response(
|
mock_resolver.return_value = [self.chall.validation(KEY.public_key())]
|
||||||
self.chall.validation_domain_name("local"),
|
|
||||||
[self.chall.validation(KEY.public_key())])
|
|
||||||
self.assertTrue(self.response.simple_verify(
|
self.assertTrue(self.response.simple_verify(
|
||||||
self.chall, "local", KEY.public_key()))
|
self.chall, "local", KEY.public_key()))
|
||||||
mock_dns.assert_called_once_with(
|
mock_resolver.assert_called_once_with(
|
||||||
self.chall.validation_domain_name("local"), "TXT")
|
self.chall.validation_domain_name("local"))
|
||||||
|
|
||||||
@mock.patch("acme.challenges.dns.resolver.query")
|
@mock.patch("acme.dns_resolver.txt_records_for_name")
|
||||||
def test_simple_verify_good_validation_multiple_txts(self, mock_dns):
|
def test_simple_verify_good_validation_multiple_txts(self, mock_resolver):
|
||||||
mock_dns.return_value = self.create_txt_response(
|
mock_resolver.return_value = ["!", self.chall.validation(KEY.public_key())]
|
||||||
self.chall.validation_domain_name("local"),
|
|
||||||
["!", self.chall.validation(KEY.public_key())])
|
|
||||||
self.assertTrue(self.response.simple_verify(
|
self.assertTrue(self.response.simple_verify(
|
||||||
self.chall, "local", KEY.public_key()))
|
self.chall, "local", KEY.public_key()))
|
||||||
mock_dns.assert_called_once_with(
|
mock_resolver.assert_called_once_with(
|
||||||
self.chall.validation_domain_name("local"), "TXT")
|
self.chall.validation_domain_name("local"))
|
||||||
|
|
||||||
@mock.patch("acme.challenges.dns.resolver.query")
|
@mock.patch("acme.dns_resolver.txt_records_for_name")
|
||||||
def test_simple_verify_bad_validation(self, mock_dns):
|
def test_simple_verify_bad_validation(self, mock_dns):
|
||||||
mock_dns.return_value = self.create_txt_response(
|
mock_dns.return_value = ["!"]
|
||||||
self.chall.validation_domain_name("local"), ["!"])
|
|
||||||
self.assertFalse(self.response.simple_verify(
|
|
||||||
self.chall, "local", KEY.public_key()))
|
|
||||||
|
|
||||||
@mock.patch("acme.challenges.dns.resolver.query")
|
|
||||||
def test_simple_verify_connection_error(self, mock_dns):
|
|
||||||
mock_dns.side_effect = dns.exception.DNSException
|
|
||||||
self.assertFalse(self.response.simple_verify(
|
self.assertFalse(self.response.simple_verify(
|
||||||
self.chall, "local", KEY.public_key()))
|
self.chall, "local", KEY.public_key()))
|
||||||
|
|
||||||
@@ -157,9 +135,8 @@ class DNS01Test(unittest.TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
from acme.challenges import DNS01
|
from acme.challenges import DNS01
|
||||||
self.msg = DNS01(
|
self.msg = DNS01(token=jose.decode_b64jose(
|
||||||
token=jose.decode_b64jose(
|
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA'))
|
||||||
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA'))
|
|
||||||
self.jmsg = {
|
self.jmsg = {
|
||||||
'type': 'dns-01',
|
'type': 'dns-01',
|
||||||
'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA',
|
'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA',
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""DNS Resolver for ACME client.
|
||||||
|
Required only for local validation of 'dns-01' challenges.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import dns.resolver
|
||||||
|
import dns.exception
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def txt_records_for_name(name):
|
||||||
|
"""Resolve the name and return the TXT records.
|
||||||
|
|
||||||
|
:param unicode name: Domain name being verified.
|
||||||
|
|
||||||
|
:returns: A list of txt records, if empty the name could not be resolved
|
||||||
|
:rtype: list of unicode
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
dns_response = dns.resolver.query(name, 'TXT')
|
||||||
|
except ImportError as error:
|
||||||
|
raise ImportError("Local validation for 'dns-01' challenges requires "
|
||||||
|
"'dnspython'")
|
||||||
|
except dns.exception.DNSException as error:
|
||||||
|
logger.error("Unable to resolve %s: %s", name, str(error))
|
||||||
|
return []
|
||||||
|
return [txt_rec for rdata in dns_response for txt_rec in rdata.strings]
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""Tests for acme.dns_resolver."""
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import dns
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from acme import dns_resolver
|
||||||
|
|
||||||
|
class TxtRecordsForNameTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def create_txt_response(self, name, txt_records):
|
||||||
|
"""
|
||||||
|
Returns an RRSet containing the 'txt_records' as the result of a DNS
|
||||||
|
query for 'name'.
|
||||||
|
|
||||||
|
This takes advantage of the fact that an Answer object mostly behaves
|
||||||
|
like an RRset.
|
||||||
|
"""
|
||||||
|
return dns.rrset.from_text_list(name, 60, "IN", "TXT", txt_records)
|
||||||
|
|
||||||
|
@mock.patch("acme.dns_resolver.dns.resolver.query")
|
||||||
|
def test_txt_records_for_name_test_with_single_response(self, mock_dns):
|
||||||
|
mock_dns.return_value = self.create_txt_response('name', ['response'])
|
||||||
|
self.assertEqual(['response'],
|
||||||
|
dns_resolver.txt_records_for_name('name'))
|
||||||
|
|
||||||
|
@mock.patch("acme.dns_resolver.dns.resolver.query")
|
||||||
|
def test_txt_records_for_name_with_multiple_responses(self, mock_dns):
|
||||||
|
mock_dns.return_value = self.create_txt_response(
|
||||||
|
'name', ['response1', 'response2'])
|
||||||
|
self.assertEqual(['response1', 'response2'],
|
||||||
|
dns_resolver.txt_records_for_name('name'))
|
||||||
|
|
||||||
|
@mock.patch("acme.dns_resolver.dns.resolver.query")
|
||||||
|
def test_txt_records_for_name_domain_not_found(self, mock_dns):
|
||||||
|
mock_dns.side_effect = dns.exception.DNSException
|
||||||
|
self.assertEquals([], dns_resolver.txt_records_for_name('name'))
|
||||||
+5
-1
@@ -12,7 +12,6 @@ install_requires = [
|
|||||||
'cryptography>=0.8',
|
'cryptography>=0.8',
|
||||||
# Connection.set_tlsext_host_name (>=0.13), X509Req.get_extensions (>=0.15)
|
# Connection.set_tlsext_host_name (>=0.13), X509Req.get_extensions (>=0.15)
|
||||||
'PyOpenSSL>=0.15',
|
'PyOpenSSL>=0.15',
|
||||||
'dnspython',
|
|
||||||
'pyrfc3339',
|
'pyrfc3339',
|
||||||
'pytz',
|
'pytz',
|
||||||
'requests',
|
'requests',
|
||||||
@@ -36,6 +35,10 @@ if sys.version_info < (2, 7, 9):
|
|||||||
install_requires.append('ndg-httpsclient')
|
install_requires.append('ndg-httpsclient')
|
||||||
install_requires.append('pyasn1')
|
install_requires.append('pyasn1')
|
||||||
|
|
||||||
|
dns_extras = [
|
||||||
|
'dnspython',
|
||||||
|
]
|
||||||
|
|
||||||
docs_extras = [
|
docs_extras = [
|
||||||
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
|
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
|
||||||
'sphinx_rtd_theme',
|
'sphinx_rtd_theme',
|
||||||
@@ -75,6 +78,7 @@ setup(
|
|||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=install_requires,
|
install_requires=install_requires,
|
||||||
extras_require={
|
extras_require={
|
||||||
|
'dns': dns_extras,
|
||||||
'docs': docs_extras,
|
'docs': docs_extras,
|
||||||
'testing': testing_extras,
|
'testing': testing_extras,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,23 +33,23 @@ setenv =
|
|||||||
|
|
||||||
[testenv:py33]
|
[testenv:py33]
|
||||||
commands =
|
commands =
|
||||||
pip install -e acme[testing]
|
pip install -e acme[dev,testing]
|
||||||
nosetests -v acme
|
nosetests -v acme
|
||||||
|
|
||||||
[testenv:py34]
|
[testenv:py34]
|
||||||
commands =
|
commands =
|
||||||
pip install -e acme[testing]
|
pip install -e acme[dev,testing]
|
||||||
nosetests -v acme
|
nosetests -v acme
|
||||||
|
|
||||||
[testenv:py35]
|
[testenv:py35]
|
||||||
commands =
|
commands =
|
||||||
pip install -e acme[testing]
|
pip install -e acme[dev,testing]
|
||||||
nosetests -v acme
|
nosetests -v acme
|
||||||
|
|
||||||
[testenv:cover]
|
[testenv:cover]
|
||||||
basepython = python2.7
|
basepython = python2.7
|
||||||
commands =
|
commands =
|
||||||
pip install -e acme -e .[testing] -e letsencrypt-apache -e letsencrypt-nginx -e letshelp-letsencrypt
|
pip install -e acme[dns] -e .[testing] -e letsencrypt-apache -e letsencrypt-nginx -e letshelp-letsencrypt
|
||||||
./tox.cover.sh
|
./tox.cover.sh
|
||||||
|
|
||||||
[testenv:lint]
|
[testenv:lint]
|
||||||
@@ -59,7 +59,7 @@ basepython = python2.7
|
|||||||
# duplicate code checking; if one of the commands fails, others will
|
# duplicate code checking; if one of the commands fails, others will
|
||||||
# continue, but tox return code will reflect previous error
|
# continue, but tox return code will reflect previous error
|
||||||
commands =
|
commands =
|
||||||
pip install -e acme -e .[dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt
|
pip install -e acme[dns] -e .[dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt
|
||||||
./pep8.travis.sh
|
./pep8.travis.sh
|
||||||
pylint --rcfile=.pylintrc letsencrypt
|
pylint --rcfile=.pylintrc letsencrypt
|
||||||
pylint --rcfile=.pylintrc acme/acme
|
pylint --rcfile=.pylintrc acme/acme
|
||||||
|
|||||||
Reference in New Issue
Block a user