From 55ca1b484fc18626e89b111ff5e960a4081694c1 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Fri, 1 Jan 2016 20:55:52 -0500 Subject: [PATCH 01/36] Initial verison of DNS-01 implementation --- acme/acme/challenges.py | 79 ++++++++++++++++++++++++++++++ acme/acme/challenges_test.py | 93 ++++++++++++++++++++++++++++++++++++ acme/setup.py | 1 + 3 files changed, 173 insertions(+) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 1e456d325..f48c20443 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -1,5 +1,8 @@ """ACME Identifier Validation Challenges.""" import abc +import base64 +import dns.resolver +import dns.exception import functools import hashlib import logging @@ -215,6 +218,82 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): self.validation(account_key, *args, **kwargs)) +@ChallengeResponse.register +class DNS01Response(KeyAuthorizationChallengeResponse): + """ACME "dns-01" challenge response.""" + typ = "dns-01" + + def simple_verify(self, chall, domain, account_public_key): + """Simple verify. + + :param challenges.DNS01 chall: Corresponding challenge. + :param unicode domain: Domain name being verified. + :param account_public_key: Public key for the key pair + being authorized. If ``None`` key verification is not + performed! + :param JWK account_public_key: + + :returns: ``True`` iff validation is successful, ``False`` + otherwise. + :rtype: bool + + """ + if not self.verify(chall, account_public_key): + logger.debug("Verification of key authorization in response failed") + return False + + validation_name = chall.validation_domain_name(domain) + validation = chall.validation(account_public_key) + logger.debug("Verifying %s at %s...", chall.typ, validation_name) + txt_records = [] + try: + dns_response = dns.resolver.query(validation_name, 'TXT') + for rdata in dns_response: + for txt_record in rdata.strings: + txt_records.append(txt_record) + except dns.exception.DNSException as error: + logger.error("Unable to resolve %s: %s", validation_name, error) + return False + + for txt_record in txt_records: + if txt_record == validation: + return True + + logger.debug("Key authorization from response (%r) doesn't match any " + "DNS response in %r", self.key_authorization, txt_records) + return False + +@Challenge.register # pylint: disable=too-many-ancestors +class DNS01(KeyAuthorizationChallenge): + """ACME "dns-01" challenge.""" + + response_cls = DNS01Response + typ = response_cls.typ + + LABEL = "_acme-challenge" + """Label clients prepend to the domain name being validated.""" + + def validation(self, account_key, **unused_kwargs): + """Generate validation. + + :param JWK account_key: + :rtype: unicode + + """ + key_authorization = self.key_authorization(account_key) + # FIXME Once boulder response according to the spec this needs to be fixed + # return base64.b64encode(hashlib.sha256(key_authorization).digest()) + return hashlib.sha256(key_authorization).hexdigest() + + def validation_domain_name(self, name): + """Domain name for TXT validation record. + + :param unicode name: Domain name being validated. + + """ + return "{0}.{1}".format(self.LABEL, name) + + @ChallengeResponse.register class HTTP01Response(KeyAuthorizationChallengeResponse): """ACME http-01 challenge response.""" diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index a4e78ebe9..a587d3f61 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -1,6 +1,7 @@ """Tests for acme.challenges.""" import unittest +import dns.rrset import mock import OpenSSL import requests @@ -76,6 +77,98 @@ class KeyAuthorizationChallengeResponseTest(unittest.TestCase): key_authorization='.foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') self.assertFalse(response.verify(self.chall, KEY.public_key())) +class DNS01ResponseTest(unittest.TestCase): + # pylint: disable=too-many-instance-attributes + + def setUp(self): + from acme.challenges import DNS01Response + self.msg = DNS01Response(key_authorization=u'foo') + self.jmsg = { + 'resource': 'challenge', + 'type': 'dns-01', + 'keyAuthorization': u'foo', + } + + from acme.challenges import DNS01 + self.chall = DNS01(token=(b'x' * 16)) + self.response = self.chall.response(KEY) + + # This takes advantage of the fact that an answer object mostly behaves like + # an RRset + def create_txt_response(self, name, txt_record): + return dns.rrset.from_text(name, 60, "IN", "TXT", txt_record) + + def test_to_partial_json(self): + self.assertEqual(self.jmsg, self.msg.to_partial_json()) + + def test_from_json(self): + from acme.challenges import DNS01Response + self.assertEqual( + self.msg, DNS01Response.from_json(self.jmsg)) + + def test_from_json_hashable(self): + from acme.challenges import DNS01Response + hash(DNS01Response.from_json(self.jmsg)) + + def test_simple_verify_bad_key_authorization(self): + key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) + self.response.simple_verify(self.chall, "local", key2.public_key()) + + @mock.patch("acme.challenges.dns.resolver.query") + def test_simple_verify_good_validation(self, mock_dns): + mock_dns.return_value = self.create_txt_response( + self.chall.validation_domain_name("local"), + self.chall.validation(KEY.public_key())) + self.assertTrue(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + mock_dns.assert_called_once_with( + self.chall.validation_domain_name("local"), "TXT") + + @mock.patch("acme.challenges.dns.resolver.query") + def test_simple_verify_bad_validation(self, mock_dns): + mock_dns.return_value = self.create_txt_response( + 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.chall, "local", KEY.public_key())) + +class DNS01Test(unittest.TestCase): + + def setUp(self): + from acme.challenges import DNS01 + self.msg = DNS01( + token=jose.decode_b64jose( + 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) + self.jmsg = { + 'type': 'dns-01', + 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', + } + + def test_validation_domain_name(self): + self.assertEqual('_acme-challenge.www.example.com', + self.msg.validation_domain_name('www.example.com')) + + def test_validation(self): + self.assertEqual( + "ac06bb8888382b6cbaddfbd48427f2f1d3f55e5ef0121990ab4a02853704dd99", + self.msg.validation(KEY)) + + def test_to_partial_json(self): + self.assertEqual(self.jmsg, self.msg.to_partial_json()) + + def test_from_json(self): + from acme.challenges import DNS01 + self.assertEqual(self.msg, DNS01.from_json(self.jmsg)) + + def test_from_json_hashable(self): + from acme.challenges import DNS01 + hash(DNS01.from_json(self.jmsg)) + class HTTP01ResponseTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes diff --git a/acme/setup.py b/acme/setup.py index ba2c88394..dd2bce5d9 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -12,6 +12,7 @@ install_requires = [ 'cryptography>=0.8', # Connection.set_tlsext_host_name (>=0.13), X509Req.get_extensions (>=0.15) 'PyOpenSSL>=0.15', + 'dnspython', 'pyrfc3339', 'pytz', 'requests', From ffc2b1ee7864a848c0f2a22ca873ac970bb911bf Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sat, 2 Jan 2016 01:42:47 -0500 Subject: [PATCH 02/36] - Lint fixes - Add test for multiple TXT records returned - Add extra parameter in DNS01.validation to select hexdigit vs. bas64 encoded validation --- acme/acme/challenges.py | 23 +++++++++++------------ acme/acme/challenges_test.py | 36 +++++++++++++++++++++++++++++------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index f48c20443..6c13ec906 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -242,17 +242,15 @@ class DNS01Response(KeyAuthorizationChallengeResponse): logger.debug("Verification of key authorization in response failed") return False - validation_name = chall.validation_domain_name(domain) + validation_domain_name = chall.validation_domain_name(domain) validation = chall.validation(account_public_key) - logger.debug("Verifying %s at %s...", chall.typ, validation_name) - txt_records = [] + logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) try: - dns_response = dns.resolver.query(validation_name, 'TXT') - for rdata in dns_response: - for txt_record in rdata.strings: - txt_records.append(txt_record) + dns_response = dns.resolver.query(validation_domain_name, 'TXT') + txt_records = sum([rdata.strings for rdata in dns_response], []) except dns.exception.DNSException as error: - logger.error("Unable to resolve %s: %s", validation_name, error) + logger.error("Unable to resolve %s: %s", validation_domain_name, + error) return False for txt_record in txt_records: @@ -273,7 +271,8 @@ class DNS01(KeyAuthorizationChallenge): LABEL = "_acme-challenge" """Label clients prepend to the domain name being validated.""" - def validation(self, account_key, **unused_kwargs): + # FIXME: Remove extra parameter once #2052 is integrated + def validation(self, account_key, dns01_hexdigit_response=True, **unused_kwargs): """Generate validation. :param JWK account_key: @@ -281,9 +280,9 @@ class DNS01(KeyAuthorizationChallenge): """ key_authorization = self.key_authorization(account_key) - # FIXME Once boulder response according to the spec this needs to be fixed - # return base64.b64encode(hashlib.sha256(key_authorization).digest()) - return hashlib.sha256(key_authorization).hexdigest() + if dns01_hexdigit_response: + return hashlib.sha256(key_authorization).hexdigest() + return base64.urlsafe_b64encode(hashlib.sha256(key_authorization).digest()) def validation_domain_name(self, name): """Domain name for TXT validation record. diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index a587d3f61..c2a629bb3 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -93,10 +93,15 @@ class DNS01ResponseTest(unittest.TestCase): self.chall = DNS01(token=(b'x' * 16)) self.response = self.chall.response(KEY) - # This takes advantage of the fact that an answer object mostly behaves like - # an RRset - def create_txt_response(self, name, txt_record): - return dns.rrset.from_text(name, 60, "IN", "TXT", txt_record) + 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): self.assertEqual(self.jmsg, self.msg.to_partial_json()) @@ -118,7 +123,17 @@ class DNS01ResponseTest(unittest.TestCase): def test_simple_verify_good_validation(self, mock_dns): mock_dns.return_value = self.create_txt_response( self.chall.validation_domain_name("local"), - self.chall.validation(KEY.public_key())) + [self.chall.validation(KEY.public_key())]) + self.assertTrue(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + mock_dns.assert_called_once_with( + self.chall.validation_domain_name("local"), "TXT") + + @mock.patch("acme.challenges.dns.resolver.query") + def test_simple_verify_good_validation_multiple_txts(self, mock_dns): + mock_dns.return_value = self.create_txt_response( + self.chall.validation_domain_name("local"), + ["!", self.chall.validation(KEY.public_key())]) self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) mock_dns.assert_called_once_with( @@ -127,7 +142,7 @@ class DNS01ResponseTest(unittest.TestCase): @mock.patch("acme.challenges.dns.resolver.query") def test_simple_verify_bad_validation(self, mock_dns): mock_dns.return_value = self.create_txt_response( - self.chall.validation_domain_name("local"), "!") + self.chall.validation_domain_name("local"), ["!"]) self.assertFalse(self.response.simple_verify( self.chall, "local", KEY.public_key())) @@ -153,10 +168,17 @@ class DNS01Test(unittest.TestCase): self.assertEqual('_acme-challenge.www.example.com', self.msg.validation_domain_name('www.example.com')) + # FIXME: Remove extra parameter once #2052 is integrated def test_validation(self): + self.assertEqual( + "rAa7iIg4K2y63fvUhCfy8dP1Xl7wEhmQq0oChTcE3Zk=", + self.msg.validation(KEY, dns01_hexdigit_response=False)) + + # FIXME: Remove this once #2052 is integrated + def test_validation_for_server_with_hexdigit_response(self): self.assertEqual( "ac06bb8888382b6cbaddfbd48427f2f1d3f55e5ef0121990ab4a02853704dd99", - self.msg.validation(KEY)) + self.msg.validation(KEY, dns01_hexdigit_response=True)) def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) From 7e2a1532ef5a3c48e3d1cf9c382a13da0ed62097 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sat, 2 Jan 2016 12:53:47 -0500 Subject: [PATCH 03/36] Move dns record retrieval into a separate method. --- acme/acme/challenges.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 6c13ec906..cc938dd31 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -223,6 +223,22 @@ class DNS01Response(KeyAuthorizationChallengeResponse): """ACME "dns-01" challenge response.""" typ = "dns-01" + def txt_records_for_name(self, name): + """Resolve the name and return the TXT records. + + :param unicode name: Domain name being verified. + + :returns: A list of txt records, or None if 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 None + return sum([rdata.strings for rdata in dns_response], []) + def simple_verify(self, chall, domain, account_public_key): """Simple verify. @@ -245,15 +261,7 @@ class DNS01Response(KeyAuthorizationChallengeResponse): validation_domain_name = chall.validation_domain_name(domain) validation = chall.validation(account_public_key) logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) - try: - dns_response = dns.resolver.query(validation_domain_name, 'TXT') - txt_records = sum([rdata.strings for rdata in dns_response], []) - except dns.exception.DNSException as error: - logger.error("Unable to resolve %s: %s", validation_domain_name, - error) - return False - - for txt_record in txt_records: + for txt_record in self.txt_records_for_domain(validation_domain_name): if txt_record == validation: return True From 64f3f53467b33d97359d0ba6f9795d5757d3f4a7 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sat, 2 Jan 2016 13:51:37 -0500 Subject: [PATCH 04/36] Fix --- acme/acme/challenges.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index cc938dd31..5c3c8e8aa 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -261,7 +261,11 @@ class DNS01Response(KeyAuthorizationChallengeResponse): validation_domain_name = chall.validation_domain_name(domain) validation = chall.validation(account_public_key) logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) - for txt_record in self.txt_records_for_domain(validation_domain_name): + txt_records = self.txt_records_for_name(validation_domain_name) + if txt_records == None: + return False + + for txt_record in txt_records: if txt_record == validation: return True From 97fb1a03f99cd219cffe7e77ab6c616c03b9afec Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 3 Jan 2016 13:19:32 -0500 Subject: [PATCH 05/36] Documentation fixes. --- acme/acme/challenges.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 5c3c8e8aa..6ca36d482 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -249,8 +249,8 @@ class DNS01Response(KeyAuthorizationChallengeResponse): performed! :param JWK account_public_key: - :returns: ``True`` iff validation is successful, ``False`` - otherwise. + :returns: ``True`` iff validation with the TXT records resolved from a + DNS server is successful. :rtype: bool """ @@ -332,8 +332,8 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): :param JWK account_public_key: :param int port: Port used in the validation. - :returns: ``True`` iff validation is successful, ``False`` - otherwise. + :returns: ``True`` iff validation of the files currently server by the + HTTP server is successful. :rtype: bool """ @@ -504,7 +504,7 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse): :returns: ``True`` iff client's control of the domain has been - verified, ``False`` otherwise. + verified. :rtype: bool """ From 74a9703269a320a0180e8360218e6f8de599d0ba Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Mon, 4 Jan 2016 16:16:42 -0500 Subject: [PATCH 06/36] Style fix --- acme/acme/challenges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 6ca36d482..b783eee9c 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -262,7 +262,7 @@ class DNS01Response(KeyAuthorizationChallengeResponse): validation = chall.validation(account_public_key) logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) txt_records = self.txt_records_for_name(validation_domain_name) - if txt_records == None: + if txt_records is None: return False for txt_record in txt_records: From 7747dc8488f2ea67b21ad53ffe9e31cd8e7d1232 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Mon, 4 Jan 2016 19:46:28 -0500 Subject: [PATCH 07/36] Remove non-compliant hexdigit encoding for dns-01 challenges (#2052 is now merged). --- acme/acme/challenges.py | 8 ++------ acme/acme/challenges_test.py | 11 ++--------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 6ca36d482..77ca2e214 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -1,6 +1,5 @@ """ACME Identifier Validation Challenges.""" import abc -import base64 import dns.resolver import dns.exception import functools @@ -283,8 +282,7 @@ class DNS01(KeyAuthorizationChallenge): LABEL = "_acme-challenge" """Label clients prepend to the domain name being validated.""" - # FIXME: Remove extra parameter once #2052 is integrated - def validation(self, account_key, dns01_hexdigit_response=True, **unused_kwargs): + def validation(self, account_key, **unused_kwargs): """Generate validation. :param JWK account_key: @@ -292,9 +290,7 @@ class DNS01(KeyAuthorizationChallenge): """ key_authorization = self.key_authorization(account_key) - if dns01_hexdigit_response: - return hashlib.sha256(key_authorization).hexdigest() - return base64.urlsafe_b64encode(hashlib.sha256(key_authorization).digest()) + return jose.b64encode(hashlib.sha256(key_authorization).digest()) def validation_domain_name(self, name): """Domain name for TXT validation record. diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index c2a629bb3..b6c771d1b 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -168,17 +168,10 @@ class DNS01Test(unittest.TestCase): self.assertEqual('_acme-challenge.www.example.com', self.msg.validation_domain_name('www.example.com')) - # FIXME: Remove extra parameter once #2052 is integrated def test_validation(self): self.assertEqual( - "rAa7iIg4K2y63fvUhCfy8dP1Xl7wEhmQq0oChTcE3Zk=", - self.msg.validation(KEY, dns01_hexdigit_response=False)) - - # FIXME: Remove this once #2052 is integrated - def test_validation_for_server_with_hexdigit_response(self): - self.assertEqual( - "ac06bb8888382b6cbaddfbd48427f2f1d3f55e5ef0121990ab4a02853704dd99", - self.msg.validation(KEY, dns01_hexdigit_response=True)) + "rAa7iIg4K2y63fvUhCfy8dP1Xl7wEhmQq0oChTcE3Zk", + self.msg.validation(KEY)) def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) From b5bb90628c080d7719546438d73b01534c7db25a Mon Sep 17 00:00:00 2001 From: wteiken Date: Tue, 5 Jan 2016 20:33:30 -0500 Subject: [PATCH 08/36] Style changes. --- acme/acme/challenges.py | 30 ++++++++++++------------------ acme/acme/challenges_test.py | 4 ++-- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 77ca2e214..cb8873984 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -1,13 +1,13 @@ """ACME Identifier Validation Challenges.""" import abc -import dns.resolver -import dns.exception import functools import hashlib import logging import socket from cryptography.hazmat.primitives import hashes +import dns.resolver +import dns.exception import OpenSSL import requests @@ -217,17 +217,16 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): self.validation(account_key, *args, **kwargs)) -@ChallengeResponse.register class DNS01Response(KeyAuthorizationChallengeResponse): """ACME "dns-01" challenge response.""" typ = "dns-01" - def txt_records_for_name(self, 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, or None if the name could not be resolved + :returns: A list of txt records, if empty the name could not be resolved :rtype: list of unicode """ @@ -235,17 +234,17 @@ class DNS01Response(KeyAuthorizationChallengeResponse): dns_response = dns.resolver.query(name, 'TXT') except dns.exception.DNSException as error: logger.error("Unable to resolve %s: %s", name, error) - return None - return sum([rdata.strings for rdata in dns_response], []) + return [] + return [txt_rec in dns_response for txt_rec in rdata.strings] +@ChallengeResponse.register def simple_verify(self, chall, domain, account_public_key): """Simple verify. :param challenges.DNS01 chall: Corresponding challenge. :param unicode domain: Domain name being verified. :param account_public_key: Public key for the key pair - being authorized. If ``None`` key verification is not - performed! + being authorized. :param JWK account_public_key: :returns: ``True`` iff validation with the TXT records resolved from a @@ -260,14 +259,9 @@ class DNS01Response(KeyAuthorizationChallengeResponse): validation_domain_name = chall.validation_domain_name(domain) validation = chall.validation(account_public_key) logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) - txt_records = self.txt_records_for_name(validation_domain_name) - if txt_records == None: - return False - - for txt_record in txt_records: - if txt_record == validation: - return True + if validation in txt_records_for_name(validation_domain_name): + return True logger.debug("Key authorization from response (%r) doesn't match any " "DNS response in %r", self.key_authorization, txt_records) return False @@ -289,8 +283,8 @@ class DNS01(KeyAuthorizationChallenge): :rtype: unicode """ - key_authorization = self.key_authorization(account_key) - return jose.b64encode(hashlib.sha256(key_authorization).digest()) + return jose.b64encode(hashlib.sha256( + self.key_authorization(account_key)).digest()) def validation_domain_name(self, name): """Domain name for TXT validation record. diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index b6c771d1b..b744de024 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -77,6 +77,7 @@ class KeyAuthorizationChallengeResponseTest(unittest.TestCase): key_authorization='.foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') self.assertFalse(response.verify(self.chall, KEY.public_key())) + class DNS01ResponseTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes @@ -108,8 +109,7 @@ class DNS01ResponseTest(unittest.TestCase): def test_from_json(self): from acme.challenges import DNS01Response - self.assertEqual( - self.msg, DNS01Response.from_json(self.jmsg)) + self.assertEqual(self.msg, DNS01Response.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import DNS01Response From 4403a78e52e684d5b94b8982f5b86609fc1fc20f Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Tue, 5 Jan 2016 22:25:24 -0500 Subject: [PATCH 09/36] Move txt_records_for_name out of class. --- acme/acme/challenges.py | 49 +++++++++++++++++++----------------- acme/acme/challenges_test.py | 1 + 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index cb8873984..e235a087e 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -217,27 +217,11 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): self.validation(account_key, *args, **kwargs)) +@ChallengeResponse.register class DNS01Response(KeyAuthorizationChallengeResponse): """ACME "dns-01" challenge response.""" typ = "dns-01" - 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 in dns_response for txt_rec in rdata.strings] - -@ChallengeResponse.register def simple_verify(self, chall, domain, account_public_key): """Simple verify. @@ -260,16 +244,18 @@ class DNS01Response(KeyAuthorizationChallengeResponse): validation = chall.validation(account_public_key) logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) - if validation in txt_records_for_name(validation_domain_name): - return True - logger.debug("Key authorization from response (%r) doesn't match any " - "DNS response in %r", self.key_authorization, txt_records) - return False + txt_records = txt_records_for_name(validation_domain_name) + exists = validation in txt_records + if not exists: + logger.debug("Key authorization from response (%r) doesn't match " + "any DNS response in %r", self.key_authorization, + txt_records) + return exists + @Challenge.register # pylint: disable=too-many-ancestors class DNS01(KeyAuthorizationChallenge): """ACME "dns-01" challenge.""" - response_cls = DNS01Response typ = response_cls.typ @@ -716,3 +702,20 @@ class DNSResponse(ChallengeResponse): """ 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] diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index b744de024..79a928456 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -152,6 +152,7 @@ class DNS01ResponseTest(unittest.TestCase): self.assertFalse(self.response.simple_verify( self.chall, "local", KEY.public_key())) + class DNS01Test(unittest.TestCase): def setUp(self): From fd2709a6fa2c2cc764aa2b1a50178ad62e71a1c5 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Tue, 5 Jan 2016 23:58:23 -0500 Subject: [PATCH 10/36] Move dnspython dependency to tests only and only import the dns.resolver when actually resolving the client. That way user code that does not call 'simple_verify' for DNS01 challenges does not depend on dnspython. --- acme/acme/challenges.py | 10 ++++++---- acme/setup.py | 3 +-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index e235a087e..aa7b20689 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -6,8 +6,6 @@ import logging import socket from cryptography.hazmat.primitives import hashes -import dns.resolver -import dns.exception import OpenSSL import requests @@ -714,8 +712,12 @@ def txt_records_for_name(name): """ try: + import dns.resolver dns_response = dns.resolver.query(name, 'TXT') - except dns.exception.DNSException as error: - logger.error("Unable to resolve %s: %s", name, error) + except ImportError as error: + raise ImportError("Local validation for 'dns-01' challenges requires " + "'dnspython'"); + except Exception 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] diff --git a/acme/setup.py b/acme/setup.py index dd2bce5d9..76a2c1b72 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -12,7 +12,6 @@ install_requires = [ 'cryptography>=0.8', # Connection.set_tlsext_host_name (>=0.13), X509Req.get_extensions (>=0.15) 'PyOpenSSL>=0.15', - 'dnspython', 'pyrfc3339', 'pytz', 'requests', @@ -76,7 +75,7 @@ setup( install_requires=install_requires, extras_require={ 'docs': docs_extras, - 'testing': testing_extras, + 'testing': testing_extras + 'dnspython', }, entry_points={ 'console_scripts': [ From 57c265c7f36087bb3de0f96a25cfcd8ae62538ab Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 00:27:07 -0500 Subject: [PATCH 11/36] Setup.py and style fixes --- acme/acme/challenges.py | 5 +++-- acme/setup.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index aa7b20689..f4eb115c4 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -713,11 +713,12 @@ def txt_records_for_name(name): """ try: import dns.resolver + import dns.exception dns_response = dns.resolver.query(name, 'TXT') except ImportError as error: raise ImportError("Local validation for 'dns-01' challenges requires " - "'dnspython'"); - except Exception as error: + "'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] diff --git a/acme/setup.py b/acme/setup.py index 76a2c1b72..e00476117 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -75,7 +75,7 @@ setup( install_requires=install_requires, extras_require={ 'docs': docs_extras, - 'testing': testing_extras + 'dnspython', + 'testing': testing_extras + ['dnspython'], }, entry_points={ 'console_scripts': [ From dc743fb57cc9c3c7fb805ece50681f1388440f9c Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 01:11:24 -0500 Subject: [PATCH 12/36] Move DNS resolver to separate module to decouple dependencies and testing. --- acme/acme/challenges.py | 30 ++++++---------------- acme/acme/challenges_test.py | 46 +++++++++------------------------- acme/acme/dns_resolver.py | 28 +++++++++++++++++++++ acme/acme/dns_resolver_test.py | 37 +++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 57 deletions(-) create mode 100644 acme/acme/dns_resolver.py create mode 100644 acme/acme/dns_resolver_test.py diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index f4eb115c4..221eb406c 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -15,7 +15,6 @@ from acme import fields from acme import jose from acme import other - logger = logging.getLogger(__name__) @@ -242,7 +241,13 @@ class DNS01Response(KeyAuthorizationChallengeResponse): validation = chall.validation(account_public_key) 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 as error: + raise ImportError("Local validation for 'dns-01' challenges " + "requires 'dnspython'") exists = validation in txt_records if not exists: logger.debug("Key authorization from response (%r) doesn't match " @@ -701,24 +706,3 @@ class DNSResponse(ChallengeResponse): """ 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: - import dns.resolver - import dns.exception - 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] diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 79a928456..0fc8fcef7 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -1,7 +1,6 @@ """Tests for acme.challenges.""" import unittest -import dns.rrset import mock import OpenSSL import requests @@ -94,16 +93,6 @@ class DNS01ResponseTest(unittest.TestCase): self.chall = DNS01(token=(b'x' * 16)) 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): 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')) self.response.simple_verify(self.chall, "local", key2.public_key()) - @mock.patch("acme.challenges.dns.resolver.query") - def test_simple_verify_good_validation(self, mock_dns): - mock_dns.return_value = self.create_txt_response( - self.chall.validation_domain_name("local"), - [self.chall.validation(KEY.public_key())]) + @mock.patch("acme.dns_resolver.txt_records_for_name") + def test_simple_verify_good_validation(self, mock_resolver): + mock_resolver.return_value = [self.chall.validation(KEY.public_key())] self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) - mock_dns.assert_called_once_with( - self.chall.validation_domain_name("local"), "TXT") + mock_resolver.assert_called_once_with( + self.chall.validation_domain_name("local")) - @mock.patch("acme.challenges.dns.resolver.query") - def test_simple_verify_good_validation_multiple_txts(self, mock_dns): - mock_dns.return_value = self.create_txt_response( - self.chall.validation_domain_name("local"), - ["!", self.chall.validation(KEY.public_key())]) + @mock.patch("acme.dns_resolver.txt_records_for_name") + def test_simple_verify_good_validation_multiple_txts(self, mock_resolver): + mock_resolver.return_value = ["!", self.chall.validation(KEY.public_key())] self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) - mock_dns.assert_called_once_with( - self.chall.validation_domain_name("local"), "TXT") + mock_resolver.assert_called_once_with( + 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): - mock_dns.return_value = self.create_txt_response( - 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 + mock_dns.return_value = ["!"] self.assertFalse(self.response.simple_verify( self.chall, "local", KEY.public_key())) diff --git a/acme/acme/dns_resolver.py b/acme/acme/dns_resolver.py new file mode 100644 index 000000000..04e52224e --- /dev/null +++ b/acme/acme/dns_resolver.py @@ -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] diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py new file mode 100644 index 000000000..d1daa2d37 --- /dev/null +++ b/acme/acme/dns_resolver_test.py @@ -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')) From a9a5e60bc59a65fb8d02f60f88946f1bafcc655c Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 01:26:32 -0500 Subject: [PATCH 13/36] Added requirements for coverage and lint. --- acme/acme/challenges.py | 2 +- acme/acme/dns_resolver_test.py | 2 +- acme/setup.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 221eb406c..22bda6445 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -245,7 +245,7 @@ class DNS01Response(KeyAuthorizationChallengeResponse): from acme import dns_resolver txt_records = dns_resolver.txt_records_for_name( validation_domain_name) - except ImportError as error: + except ImportError: raise ImportError("Local validation for 'dns-01' challenges " "requires 'dnspython'") exists = validation in txt_records diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index d1daa2d37..2202ce4e8 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -23,7 +23,7 @@ class TxtRecordsForNameTest(unittest.TestCase): 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( diff --git a/acme/setup.py b/acme/setup.py index e00476117..269d9e1fd 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -74,7 +74,9 @@ setup( include_package_data=True, install_requires=install_requires, extras_require={ + 'coverage': ['dnspython'], 'docs': docs_extras, + 'lint': ['dnspython'], 'testing': testing_extras + ['dnspython'], }, entry_points={ From d2ced2de6aa31d6fe2aab4b68a7feb89aad307d1 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 01:48:12 -0500 Subject: [PATCH 14/36] Dep fixes for lint/coverage. --- acme/setup.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 269d9e1fd..e4a525605 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -35,6 +35,10 @@ if sys.version_info < (2, 7, 9): install_requires.append('ndg-httpsclient') install_requires.append('pyasn1') +dev_extras = [ + 'dnspython', +] + docs_extras = [ 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags 'sphinx_rtd_theme', @@ -42,6 +46,7 @@ docs_extras = [ ] testing_extras = [ + 'dnspython', 'nose', 'tox', ] @@ -74,10 +79,9 @@ setup( include_package_data=True, install_requires=install_requires, extras_require={ - 'coverage': ['dnspython'], + 'dev': dev_extras, 'docs': docs_extras, - 'lint': ['dnspython'], - 'testing': testing_extras + ['dnspython'], + 'testing': testing_extras, }, entry_points={ 'console_scripts': [ From 52c487f462067ca810f3f3b840876306dfd1e9b1 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 02:44:19 -0500 Subject: [PATCH 15/36] Add new 'test' extras and update tox.ini accordingly. --- acme/acme/challenges_test.py | 5 ++--- acme/setup.py | 5 ++--- tox.ini | 10 +++++----- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 0fc8fcef7..ac94619b5 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -135,9 +135,8 @@ class DNS01Test(unittest.TestCase): def setUp(self): from acme.challenges import DNS01 - self.msg = DNS01( - token=jose.decode_b64jose( - 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) + self.msg = DNS01(token=jose.decode_b64jose( + 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) self.jmsg = { 'type': 'dns-01', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', diff --git a/acme/setup.py b/acme/setup.py index e4a525605..3cb69ce9e 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -35,7 +35,7 @@ if sys.version_info < (2, 7, 9): install_requires.append('ndg-httpsclient') install_requires.append('pyasn1') -dev_extras = [ +dns_extras = [ 'dnspython', ] @@ -46,7 +46,6 @@ docs_extras = [ ] testing_extras = [ - 'dnspython', 'nose', 'tox', ] @@ -79,7 +78,7 @@ setup( include_package_data=True, install_requires=install_requires, extras_require={ - 'dev': dev_extras, + 'dns': dns_extras, 'docs': docs_extras, 'testing': testing_extras, }, diff --git a/tox.ini b/tox.ini index 1abe1cf39..98e185cfa 100644 --- a/tox.ini +++ b/tox.ini @@ -33,23 +33,23 @@ setenv = [testenv:py33] commands = - pip install -e acme[testing] + pip install -e acme[dev,testing] nosetests -v acme [testenv:py34] commands = - pip install -e acme[testing] + pip install -e acme[dev,testing] nosetests -v acme [testenv:py35] commands = - pip install -e acme[testing] + pip install -e acme[dev,testing] nosetests -v acme [testenv:cover] basepython = python2.7 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 [testenv:lint] @@ -59,7 +59,7 @@ basepython = python2.7 # duplicate code checking; if one of the commands fails, others will # continue, but tox return code will reflect previous error commands = - pip install -e acme -e .[dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt + pip install -e acme -e .[dns,dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt ./pep8.travis.sh pylint --rcfile=.pylintrc letsencrypt pylint --rcfile=.pylintrc acme/acme From cead22f4a7d46ff921a66bd049317c78733c596b Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 02:45:20 -0500 Subject: [PATCH 16/36] Add dns env to lint/cover --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 98e185cfa..a1ed23cf7 100644 --- a/tox.ini +++ b/tox.ini @@ -59,7 +59,7 @@ basepython = python2.7 # duplicate code checking; if one of the commands fails, others will # continue, but tox return code will reflect previous error commands = - pip install -e acme -e .[dns,dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt + pip install -e acme[dns] -e .[dns,dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt ./pep8.travis.sh pylint --rcfile=.pylintrc letsencrypt pylint --rcfile=.pylintrc acme/acme From e61e83f7e26803b556c7e6620cd014779d1f5e36 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 02:46:29 -0500 Subject: [PATCH 17/36] tox.ini fix --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index a1ed23cf7..cdc2c2396 100644 --- a/tox.ini +++ b/tox.ini @@ -59,7 +59,7 @@ basepython = python2.7 # duplicate code checking; if one of the commands fails, others will # continue, but tox return code will reflect previous error commands = - pip install -e acme[dns] -e .[dns,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 pylint --rcfile=.pylintrc letsencrypt pylint --rcfile=.pylintrc acme/acme From b8a9c2597c4ba6a02ed9b530c1286a1c24b7ccac Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 02:57:53 -0500 Subject: [PATCH 18/36] add dns environment to pyXX --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index cdc2c2396..4a7aea0d0 100644 --- a/tox.ini +++ b/tox.ini @@ -33,17 +33,17 @@ setenv = [testenv:py33] commands = - pip install -e acme[dev,testing] + pip install -e acme[dns,testing] nosetests -v acme [testenv:py34] commands = - pip install -e acme[dev,testing] + pip install -e acme[dns,testing] nosetests -v acme [testenv:py35] commands = - pip install -e acme[dev,testing] + pip install -e acme[dns,testing] nosetests -v acme [testenv:cover] From b73b410729e90bf0c6c1882130f6c0759675d7c3 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 02:59:25 -0500 Subject: [PATCH 19/36] Exclude import error case from coverage in dns_resolver --- acme/acme/dns_resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/dns_resolver.py b/acme/acme/dns_resolver.py index 04e52224e..e28f132f1 100644 --- a/acme/acme/dns_resolver.py +++ b/acme/acme/dns_resolver.py @@ -19,7 +19,7 @@ def txt_records_for_name(name): """ try: dns_response = dns.resolver.query(name, 'TXT') - except ImportError as error: + except ImportError as error: # pragma: no cover raise ImportError("Local validation for 'dns-01' challenges requires " "'dnspython'") except dns.exception.DNSException as error: From 6bc3060fbb6a3d248654b6f97c004ce682e1e52f Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 03:11:09 -0500 Subject: [PATCH 20/36] More fixes for travis tests --- acme/acme/challenges.py | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 22bda6445..08d847627 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -245,7 +245,7 @@ class DNS01Response(KeyAuthorizationChallengeResponse): from acme import dns_resolver txt_records = dns_resolver.txt_records_for_name( validation_domain_name) - except ImportError: + except ImportError: # pragma: no cover raise ImportError("Local validation for 'dns-01' challenges " "requires 'dnspython'") exists = validation in txt_records diff --git a/tox.ini b/tox.ini index 4a7aea0d0..c0a819b5e 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ envlist = py26,py27,py33,py34,py35,cover,lint # packages installed separately to ensure that dowstream deps problems # are detected, c.f. #1002 commands = - pip install -e acme[testing] + pip install -e acme[dns,testing] nosetests -v acme pip install -e .[testing] nosetests -v letsencrypt From 02a493011ef039ca6ec1d52d9c87d4317afe0d12 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 22:56:59 -0500 Subject: [PATCH 21/36] Remove superfluous except: and change Exception returned if dnspython is not available. --- acme/acme/challenges.py | 4 ++-- acme/acme/dns_resolver.py | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 08d847627..3a78e403a 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -246,8 +246,8 @@ class DNS01Response(KeyAuthorizationChallengeResponse): txt_records = dns_resolver.txt_records_for_name( validation_domain_name) except ImportError: # pragma: no cover - raise ImportError("Local validation for 'dns-01' challenges " - "requires 'dnspython'") + raise errors.Error("Local validation for 'dns-01' challenges " + "requires 'dnspython'") exists = validation in txt_records if not exists: logger.debug("Key authorization from response (%r) doesn't match " diff --git a/acme/acme/dns_resolver.py b/acme/acme/dns_resolver.py index e28f132f1..3572ee277 100644 --- a/acme/acme/dns_resolver.py +++ b/acme/acme/dns_resolver.py @@ -19,9 +19,6 @@ def txt_records_for_name(name): """ try: dns_response = dns.resolver.query(name, 'TXT') - except ImportError as error: # pragma: no cover - 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 [] From 446994e8ef11a860ac94a52751bc51a9fded6b04 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sat, 9 Jan 2016 14:58:19 -0500 Subject: [PATCH 22/36] Limit length of try block. --- acme/acme/challenges.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 3a78e403a..493d230d8 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -243,11 +243,10 @@ class DNS01Response(KeyAuthorizationChallengeResponse): try: from acme import dns_resolver - txt_records = dns_resolver.txt_records_for_name( - validation_domain_name) except ImportError: # pragma: no cover raise errors.Error("Local validation for 'dns-01' challenges " "requires 'dnspython'") + txt_records = dns_resolver.txt_records_for_name(validation_domain_name) exists = validation in txt_records if not exists: logger.debug("Key authorization from response (%r) doesn't match " From 56154f1301725dff575e22011a81ca18d5729307 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sat, 9 Jan 2016 16:43:43 -0500 Subject: [PATCH 23/36] - Use dnspython3 fir py3X environments. - Fix encoding for simple_verify. --- acme/acme/challenges.py | 4 ++-- acme/setup.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 493d230d8..3fa259b13 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -271,8 +271,8 @@ class DNS01(KeyAuthorizationChallenge): :rtype: unicode """ - return jose.b64encode(hashlib.sha256( - self.key_authorization(account_key)).digest()) + return jose.b64encode(hashlib.sha256(self.key_authorization( + account_key).encode("utf-8")).digest()).decode() def validation_domain_name(self, name): """Domain name for TXT validation record. diff --git a/acme/setup.py b/acme/setup.py index c54dc42e6..df5cfba0e 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -35,9 +35,14 @@ if sys.version_info < (2, 7, 9): install_requires.append('ndg-httpsclient') install_requires.append('pyasn1') -dns_extras = [ - 'dnspython', -] +if sys.version_info < (3, 0): + dns_extras = [ + 'dnspython', + ] +else: + dns_extras = [ + 'dnspython3', + ] docs_extras = [ 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags From d842f268e5414f418e69913f6ac4fadffb327fb1 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sat, 9 Jan 2016 17:07:20 -0500 Subject: [PATCH 24/36] - Use dnspython3 fir py3X environments. - Fix encoding for simple_verify. --- acme/acme/challenges.py | 4 ++-- acme/setup.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 493d230d8..3fa259b13 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -271,8 +271,8 @@ class DNS01(KeyAuthorizationChallenge): :rtype: unicode """ - return jose.b64encode(hashlib.sha256( - self.key_authorization(account_key)).digest()) + return jose.b64encode(hashlib.sha256(self.key_authorization( + account_key).encode("utf-8")).digest()).decode() def validation_domain_name(self, name): """Domain name for TXT validation record. diff --git a/acme/setup.py b/acme/setup.py index c54dc42e6..df5cfba0e 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -35,9 +35,14 @@ if sys.version_info < (2, 7, 9): install_requires.append('ndg-httpsclient') install_requires.append('pyasn1') -dns_extras = [ - 'dnspython', -] +if sys.version_info < (3, 0): + dns_extras = [ + 'dnspython', + ] +else: + dns_extras = [ + 'dnspython3', + ] docs_extras = [ 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags From f2b52bd830457200474e98394b8aff0315e9d51f Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 10 Jan 2016 12:58:29 -0500 Subject: [PATCH 25/36] Fix dcumentation --- acme/acme/challenges.py | 1 - 1 file changed, 1 deletion(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 3fa259b13..a5b142c5b 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -226,7 +226,6 @@ class DNS01Response(KeyAuthorizationChallengeResponse): :param unicode domain: Domain name being verified. :param account_public_key: Public key for the key pair being authorized. - :param JWK account_public_key: :returns: ``True`` iff validation with the TXT records resolved from a DNS server is successful. From 49c40e7a584d1d8faeac4436c6c7787e44a12b6d Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 10 Jan 2016 13:00:38 -0500 Subject: [PATCH 26/36] Skip dns_resolver tests if dnspython is not available. --- acme/acme/dns_resolver_test.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 2202ce4e8..0d5cc2543 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -1,11 +1,17 @@ """Tests for acme.dns_resolver.""" import unittest -import dns import mock +try: + import dns +except ImportError: + dns = None + from acme import dns_resolver +@unittest.skipIf(dns is None, + "dnspython is not available, skipping dns_resolver tests") class TxtRecordsForNameTest(unittest.TestCase): def create_txt_response(self, name, txt_records): From cfe56cbd925a4c90a051355f4f891ee8f15308d0 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 10 Jan 2016 17:00:14 -0500 Subject: [PATCH 27/36] 2.6 compatible skipping of tests. --- acme/acme/dns_resolver_test.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 0d5cc2543..54f074a92 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -1,17 +1,15 @@ """Tests for acme.dns_resolver.""" import unittest - import mock -try: - import dns -except ImportError: - dns = None - from acme import dns_resolver -@unittest.skipIf(dns is None, - "dnspython is not available, skipping dns_resolver tests") +try: + import dns +except ImportError: # pragma: no cover + dns = None + + class TxtRecordsForNameTest(unittest.TestCase): def create_txt_response(self, name, txt_records): @@ -41,3 +39,9 @@ class TxtRecordsForNameTest(unittest.TestCase): 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')) + + def run(self, result=None): + if dns is None: + print self, "... SKIPPING, no dnspython available" + return + super(TxtRecordsForNameTest, self).run(result) From 0010610a4a038fd592c0544d02fd0f36a8ff3495 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 10 Jan 2016 17:06:03 -0500 Subject: [PATCH 28/36] py3X fix --- acme/acme/dns_resolver_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 54f074a92..8e92cd4a3 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -42,6 +42,6 @@ class TxtRecordsForNameTest(unittest.TestCase): def run(self, result=None): if dns is None: - print self, "... SKIPPING, no dnspython available" + print(self, "... SKIPPING, no dnspython available") return super(TxtRecordsForNameTest, self).run(result) From 2d8de74f4abd659cad518ec5d74d8613fe9830ac Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 10 Jan 2016 17:13:25 -0500 Subject: [PATCH 29/36] pcoverage fix --- acme/acme/dns_resolver_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 8e92cd4a3..770a95a77 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -7,7 +7,7 @@ from acme import dns_resolver try: import dns except ImportError: # pragma: no cover - dns = None + dns = Nones class TxtRecordsForNameTest(unittest.TestCase): @@ -41,7 +41,7 @@ class TxtRecordsForNameTest(unittest.TestCase): self.assertEquals([], dns_resolver.txt_records_for_name('name')) def run(self, result=None): - if dns is None: + if dns is None: # pragma: no cover print(self, "... SKIPPING, no dnspython available") return super(TxtRecordsForNameTest, self).run(result) From 1ff121b616c8d599549be19a07bd45fb8429fc49 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 10 Jan 2016 18:08:16 -0500 Subject: [PATCH 30/36] pcoverage fix --- acme/acme/dns_resolver_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 770a95a77..5d160fc07 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -7,7 +7,7 @@ from acme import dns_resolver try: import dns except ImportError: # pragma: no cover - dns = Nones + dns = None class TxtRecordsForNameTest(unittest.TestCase): From 9179276cb982945f6d6083b26d7a55d0112894e9 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 10 Jan 2016 20:59:11 -0500 Subject: [PATCH 31/36] Modify dns_resolver_test to skip tests if dnspython is not available. --- acme/acme/dns_resolver_test.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 2202ce4e8..5d160fc07 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -1,11 +1,15 @@ """Tests for acme.dns_resolver.""" import unittest - -import dns import mock from acme import dns_resolver +try: + import dns +except ImportError: # pragma: no cover + dns = None + + class TxtRecordsForNameTest(unittest.TestCase): def create_txt_response(self, name, txt_records): @@ -35,3 +39,9 @@ class TxtRecordsForNameTest(unittest.TestCase): 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')) + + def run(self, result=None): + if dns is None: # pragma: no cover + print(self, "... SKIPPING, no dnspython available") + return + super(TxtRecordsForNameTest, self).run(result) From 05a61c181b277d4a3f94abe038da0fda549faeda Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 10 Jan 2016 21:42:20 -0500 Subject: [PATCH 32/36] Lint fixes. --- acme/acme/challenges_test.py | 2 +- acme/acme/dns_resolver_test.py | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index ac94619b5..b32bebed0 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -114,7 +114,7 @@ class DNS01ResponseTest(unittest.TestCase): self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) mock_resolver.assert_called_once_with( - self.chall.validation_domain_name("local")) + self.chall.validation_domain_name("local")) @mock.patch("acme.dns_resolver.txt_records_for_name") def test_simple_verify_good_validation_multiple_txts(self, mock_resolver): diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 5d160fc07..39645c492 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -9,28 +9,28 @@ try: except ImportError: # pragma: no cover dns = None +def create_txt_response(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) + 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']) + def test_txt_records_for_name_with_single_response(self, mock_dns): + mock_dns.return_value = 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( + mock_dns.return_value = create_txt_response( 'name', ['response1', 'response2']) self.assertEqual(['response1', 'response2'], dns_resolver.txt_records_for_name('name')) From c15581bcfd4b9ffe8d1e1ceb03e316c337691524 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Thu, 14 Jan 2016 23:37:05 -0500 Subject: [PATCH 33/36] Fix lint problems. --- acme/acme/challenges_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 39f7449f2..af4b81f80 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -119,7 +119,8 @@ class DNS01ResponseTest(unittest.TestCase): @mock.patch("acme.dns_resolver.txt_records_for_name") def test_simple_verify_good_validation_multiple_txts(self, mock_resolver): - mock_resolver.return_value = ["!", self.chall.validation(KEY.public_key())] + mock_resolver.return_value = [ + "!", self.chall.validation(KEY.public_key())] self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) mock_resolver.assert_called_once_with( From 7c3271545fe48de9c67b2781ab1fd58328e4e095 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sat, 13 Feb 2016 01:05:35 -0500 Subject: [PATCH 34/36] Do not log an error when getting NXDOMAIN. --- acme/acme/dns_resolver.py | 4 +++- acme/acme/dns_resolver_test.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/acme/acme/dns_resolver.py b/acme/acme/dns_resolver.py index 3572ee277..badbf486a 100644 --- a/acme/acme/dns_resolver.py +++ b/acme/acme/dns_resolver.py @@ -19,7 +19,9 @@ def txt_records_for_name(name): """ try: dns_response = dns.resolver.query(name, 'TXT') + except dns.resolver.NXDOMAIN as error: + return [] except dns.exception.DNSException as error: - logger.error("Unable to resolve %s: %s", name, str(error)) + logger.error("Error resolving %s: %s", name, str(error)) return [] return [txt_rec for rdata in dns_response for txt_rec in rdata.strings] diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 80481343d..53fc0cc77 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -38,6 +38,11 @@ class TxtRecordsForNameTest(unittest.TestCase): @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.resolver.NXDOMAIN + self.assertEquals([], dns_resolver.txt_records_for_name('name')) + + @mock.patch("acme.dns_resolver.dns.resolver.query") + def test_txt_records_for_name_domain_other_error(self, mock_dns): mock_dns.side_effect = dns.exception.DNSException self.assertEquals([], dns_resolver.txt_records_for_name('name')) From 9396e92a966cbc07c0a69b7148f760b4337823dd Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Mon, 25 Apr 2016 00:46:45 -0400 Subject: [PATCH 35/36] Fix lint issues. --- acme/acme/challenges.py | 1 - acme/acme/dns_resolver.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index d27719449..f611578dd 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -599,4 +599,3 @@ class DNSResponse(ChallengeResponse): """ return chall.check_validation(self.validation, account_public_key) - diff --git a/acme/acme/dns_resolver.py b/acme/acme/dns_resolver.py index badbf486a..15638e5d0 100644 --- a/acme/acme/dns_resolver.py +++ b/acme/acme/dns_resolver.py @@ -8,6 +8,7 @@ import dns.exception logger = logging.getLogger(__name__) + def txt_records_for_name(name): """Resolve the name and return the TXT records. From b2505b996fe4739739f2ec57bac0d6b9a99e226b Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 31 Jul 2016 20:36:00 -0400 Subject: [PATCH 36/36] Switch to always using dnspython (requires dnspthon>=1.12). Also, address some documentation nits. --- acme/acme/challenges.py | 8 ++++---- acme/acme/dns_resolver.py | 4 +++- acme/setup.py | 12 ++++-------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 23841c2b4..6242c376c 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -207,7 +207,7 @@ class KeyAuthorizationChallenge(_TokenChallenge): @ChallengeResponse.register class DNS01Response(KeyAuthorizationChallengeResponse): - """ACME "dns-01" challenge response.""" + """ACME dns-01 challenge response.""" typ = "dns-01" def simple_verify(self, chall, domain, account_public_key): @@ -215,7 +215,7 @@ class DNS01Response(KeyAuthorizationChallengeResponse): :param challenges.DNS01 chall: Corresponding challenge. :param unicode domain: Domain name being verified. - :param account_public_key: Public key for the key pair + :param JWK account_public_key: Public key for the key pair being authorized. :returns: ``True`` iff validation with the TXT records resolved from a @@ -247,7 +247,7 @@ class DNS01Response(KeyAuthorizationChallengeResponse): @Challenge.register # pylint: disable=too-many-ancestors class DNS01(KeyAuthorizationChallenge): - """ACME "dns-01" challenge.""" + """ACME dns-01 challenge.""" response_cls = DNS01Response typ = response_cls.typ @@ -298,7 +298,7 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): being authorized. :param int port: Port used in the validation. - :returns: ``True`` iff validation of the files currently server by the + :returns: ``True`` iff validation with the files currently served by the HTTP server is successful. :rtype: bool diff --git a/acme/acme/dns_resolver.py b/acme/acme/dns_resolver.py index 15638e5d0..f551c6095 100644 --- a/acme/acme/dns_resolver.py +++ b/acme/acme/dns_resolver.py @@ -25,4 +25,6 @@ def txt_records_for_name(name): except dns.exception.DNSException as error: logger.error("Error resolving %s: %s", name, str(error)) return [] - return [txt_rec for rdata in dns_response for txt_rec in rdata.strings] + + return [txt_rec.decode("utf-8") for rdata in dns_response + for txt_rec in rdata.strings] diff --git a/acme/setup.py b/acme/setup.py index 75565f7f8..94f78d4cd 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -35,14 +35,10 @@ if sys.version_info < (2, 7): else: install_requires.append('mock') -if sys.version_info < (3, 0): - dns_extras = [ - 'dnspython', - ] -else: - dns_extras = [ - 'dnspython3', - ] +# dnspython 1.12 is required to support both Python 2 and Python 3. +dns_extras = [ + 'dnspython>=1.12', +] dev_extras = [ 'nose',