From 10b8a42c90d037880fbfc81fb71adb27251252e3 Mon Sep 17 00:00:00 2001 From: Daniel Robbins Date: Thu, 21 Dec 2017 09:24:40 -0700 Subject: [PATCH 1/2] Update DNSSEC code to use pycryptodome instead of pycrypto. These changes make dnspython *incompatible* with pycrypto -- pycryptodome must be used. The ecdsa module continues to be used for ECDSA support. --- ChangeLog | 5 ++ dns/__init__.py | 1 - dns/dnssec.py | 161 +++++++++++++++++++++++++++------------------------ dns/hash.py | 31 ---------- dns/tsig.py | 4 +- doc/dnssec.rst | 9 ++- doc/installation.rst | 4 +- tests/test_dnssec.py | 16 ++--- 8 files changed, 105 insertions(+), 126 deletions(-) delete mode 100644 dns/hash.py diff --git a/ChangeLog b/ChangeLog index e796638..43e5f4b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2017-12-21 Daniel Robbins + + * dns/dnssec.py: migrated code from pycrypto (apparently no + longer maintained) to pycryptodome. All tests passing. + 2017-01-02 Bob Halley * dns/e164.py: to_e164() was returning binary instead of text, diff --git a/dns/__init__.py b/dns/__init__.py index c848e48..3852729 100644 --- a/dns/__init__.py +++ b/dns/__init__.py @@ -22,7 +22,6 @@ 'entropy', 'exception', 'flags', - 'hash', 'inet', 'ipv4', 'ipv6', diff --git a/dns/dnssec.py b/dns/dnssec.py index b91a64f..2b5d5b2 100644 --- a/dns/dnssec.py +++ b/dns/dnssec.py @@ -20,7 +20,6 @@ import time import dns.exception -import dns.hash import dns.name import dns.node import dns.rdataset @@ -28,7 +27,8 @@ import dns.rdatatype import dns.rdataclass from ._compat import string_types - +from Crypto.Hash import MD5, SHA1, SHA256, SHA384, SHA512 +from Crypto.Signature import pkcs1_15, DSS class UnsupportedAlgorithm(dns.exception.DNSException): """The DNSSEC algorithm is not supported.""" @@ -39,27 +39,27 @@ class ValidationFailure(dns.exception.DNSException): #: RSAMD5 -RSAMD5 = 1 +ALGO_RSAMD5 = 1 #: DH -DH = 2 +ALGO_DH = 2 #: DSA -DSA = 3 +ALGO_DSA = 3 #: ECC -ECC = 4 +ALGO_ECC = 4 #: RSASHA1 -RSASHA1 = 5 +ALGO_RSASHA1 = 5 #: DSANSEC3SHA1 -DSANSEC3SHA1 = 6 +ALGO_DSANSEC3SHA1 = 6 #: RSASHA1NSEC3SHA1 -RSASHA1NSEC3SHA1 = 7 +ALGO_RSASHA1NSEC3SHA1 = 7 #: RSASHA256 -RSASHA256 = 8 +ALGO_RSASHA256 = 8 #: RSASHA512 -RSASHA512 = 10 +ALGO_RSASHA512 = 10 #: ECDSAP256SHA256 -ECDSAP256SHA256 = 13 +ALGO_ECDSAP256SHA256 = 13 #: ECDSAP384SHA384 -ECDSAP384SHA384 = 14 +ALGO_ECDSAP384SHA384 = 14 #: INDIRECT INDIRECT = 252 #: PRIVATEDNS @@ -68,18 +68,18 @@ class ValidationFailure(dns.exception.DNSException): PRIVATEOID = 254 _algorithm_by_text = { - 'RSAMD5': RSAMD5, - 'DH': DH, - 'DSA': DSA, - 'ECC': ECC, - 'RSASHA1': RSASHA1, - 'DSANSEC3SHA1': DSANSEC3SHA1, - 'RSASHA1NSEC3SHA1': RSASHA1NSEC3SHA1, - 'RSASHA256': RSASHA256, - 'RSASHA512': RSASHA512, + 'RSAMD5': ALGO_RSAMD5, + 'DH': ALGO_DH, + 'DSA': ALGO_DSA, + 'ECC': ALGO_ECC, + 'RSASHA1': ALGO_RSASHA1, + 'DSANSEC3SHA1': ALGO_DSANSEC3SHA1, + 'RSASHA1NSEC3SHA1': ALGO_RSASHA1NSEC3SHA1, + 'RSASHA256': ALGO_RSASHA256, + 'RSASHA512': ALGO_RSASHA512, 'INDIRECT': INDIRECT, - 'ECDSAP256SHA256': ECDSAP256SHA256, - 'ECDSAP384SHA384': ECDSAP384SHA384, + 'ECDSAP256SHA256': ALGO_ECDSAP256SHA256, + 'ECDSAP384SHA384': ALGO_ECDSAP384SHA384, 'PRIVATEDNS': PRIVATEDNS, 'PRIVATEOID': PRIVATEOID, } @@ -132,7 +132,7 @@ def key_id(key, origin=None): rdata = _to_rdata(key, origin) rdata = bytearray(rdata) - if key.algorithm == RSAMD5: + if key.algorithm == ALGO_RSAMD5: return (rdata[-3] << 8) + rdata[-2] else: total = 0 @@ -164,10 +164,10 @@ def make_ds(name, key, algorithm, origin=None): if algorithm.upper() == 'SHA1': dsalg = 1 - hash = dns.hash.hashes['SHA1']() + hash = SHA1.new() elif algorithm.upper() == 'SHA256': dsalg = 2 - hash = dns.hash.hashes['SHA256']() + hash = SHA256.new() else: raise UnsupportedAlgorithm('unsupported algorithm "%s"' % algorithm) @@ -203,51 +203,51 @@ def _find_candidate_keys(keys, rrsig): def _is_rsa(algorithm): - return algorithm in (RSAMD5, RSASHA1, - RSASHA1NSEC3SHA1, RSASHA256, - RSASHA512) + return algorithm in (ALGO_RSAMD5, ALGO_RSASHA1, + ALGO_RSASHA1NSEC3SHA1, ALGO_RSASHA256, + ALGO_RSASHA512) def _is_dsa(algorithm): - return algorithm in (DSA, DSANSEC3SHA1) + return algorithm in (ALGO_DSA, ALGO_DSANSEC3SHA1) def _is_ecdsa(algorithm): - return _have_ecdsa and (algorithm in (ECDSAP256SHA256, ECDSAP384SHA384)) + return _have_ecdsa and (algorithm in (ALGO_ECDSAP256SHA256, ALGO_ECDSAP384SHA384)) def _is_md5(algorithm): - return algorithm == RSAMD5 + return algorithm == ALGO_RSAMD5 def _is_sha1(algorithm): - return algorithm in (DSA, RSASHA1, - DSANSEC3SHA1, RSASHA1NSEC3SHA1) + return algorithm in (ALGO_DSA, ALGO_RSASHA1, + ALGO_DSANSEC3SHA1, ALGO_RSASHA1NSEC3SHA1) def _is_sha256(algorithm): - return algorithm in (RSASHA256, ECDSAP256SHA256) + return algorithm in (ALGO_RSASHA256, ALGO_ECDSAP256SHA256) def _is_sha384(algorithm): - return algorithm == ECDSAP384SHA384 + return algorithm == ALGO_ECDSAP384SHA384 def _is_sha512(algorithm): - return algorithm == RSASHA512 + return algorithm == ALGO_RSASHA512 def _make_hash(algorithm): if _is_md5(algorithm): - return dns.hash.hashes['MD5']() + return MD5.new() if _is_sha1(algorithm): - return dns.hash.hashes['SHA1']() + return SHA1.new() if _is_sha256(algorithm): - return dns.hash.hashes['SHA256']() + return SHA256.new() if _is_sha384(algorithm): - return dns.hash.hashes['SHA384']() + return SHA384.new() if _is_sha512(algorithm): - return dns.hash.hashes['SHA512']() + return SHA512.new() raise ValidationFailure('unknown hash for algorithm %u' % algorithm) @@ -326,11 +326,13 @@ def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None): keyptr = keyptr[2:] rsa_e = keyptr[0:bytes_] rsa_n = keyptr[bytes_:] - keylen = len(rsa_n) * 8 - pubkey = Crypto.PublicKey.RSA.construct( - (Crypto.Util.number.bytes_to_long(rsa_n), - Crypto.Util.number.bytes_to_long(rsa_e))) - sig = (Crypto.Util.number.bytes_to_long(rrsig.signature),) + try: + pubkey = Crypto.PublicKey.RSA.construct( + (Crypto.Util.number.bytes_to_long(rsa_n), + Crypto.Util.number.bytes_to_long(rsa_e))) + except ValueError: + raise ValidationFailure('invalid public key') + sig = rrsig.signature elif _is_dsa(rrsig.algorithm): keyptr = candidate_key.key (t,) = struct.unpack('!B', keyptr[0:1]) @@ -348,20 +350,19 @@ def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None): Crypto.Util.number.bytes_to_long(dsa_g), Crypto.Util.number.bytes_to_long(dsa_p), Crypto.Util.number.bytes_to_long(dsa_q))) - (dsa_r, dsa_s) = struct.unpack('!20s20s', rrsig.signature[1:]) - sig = (Crypto.Util.number.bytes_to_long(dsa_r), - Crypto.Util.number.bytes_to_long(dsa_s)) + sig = rrsig.signature[1:] elif _is_ecdsa(rrsig.algorithm): - if rrsig.algorithm == ECDSAP256SHA256: + # use ecdsa for NIST-384p -- not currently supported by pycryptodome + + keyptr = candidate_key.key + + if rrsig.algorithm == ALGO_ECDSAP256SHA256: curve = ecdsa.curves.NIST256p key_len = 32 - elif rrsig.algorithm == ECDSAP384SHA384: + elif rrsig.algorithm == ALGO_ECDSAP384SHA384: curve = ecdsa.curves.NIST384p key_len = 48 - else: - # shouldn't happen - raise ValidationFailure('unknown ECDSA curve') - keyptr = candidate_key.key + x = Crypto.Util.number.bytes_to_long(keyptr[0:key_len]) y = Crypto.Util.number.bytes_to_long(keyptr[key_len:key_len * 2]) if not ecdsa.ecdsa.point_is_valid(curve.generator, x, y): @@ -374,6 +375,7 @@ def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None): s = rrsig.signature[key_len:] sig = ecdsa.ecdsa.Signature(Crypto.Util.number.bytes_to_long(r), Crypto.Util.number.bytes_to_long(s)) + else: raise ValidationFailure('unknown algorithm %u' % rrsig.algorithm) @@ -395,24 +397,31 @@ def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None): hash.update(rrlen) hash.update(rrdata) - digest = hash.digest() - - if _is_rsa(rrsig.algorithm): - # PKCS1 algorithm identifier goop - digest = _make_algorithm_id(rrsig.algorithm) + digest - padlen = keylen // 8 - len(digest) - 3 - digest = struct.pack('!%dB' % (2 + padlen + 1), - *([0, 1] + [0xFF] * padlen + [0])) + digest - elif _is_dsa(rrsig.algorithm) or _is_ecdsa(rrsig.algorithm): - pass - else: - # Raise here for code clarity; this won't actually ever happen - # since if the algorithm is really unknown we'd already have - # raised an exception above - raise ValidationFailure('unknown algorithm %u' % rrsig.algorithm) - - if pubkey.verify(digest, sig): + try: + if _is_rsa(rrsig.algorithm): + verifier = pkcs1_15.new(pubkey) + # will raise ValueError if verify fails: + verifier.verify(hash, sig) + elif _is_dsa(rrsig.algorithm): + verifier = DSS.new(pubkey, 'fips-186-3') + verifier.verify(hash, sig) + elif _is_ecdsa(rrsig.algorithm): + digest = hash.digest() + if pubkey.verify(digest, sig): + return + else: + raise ValueError + else: + # Raise here for code clarity; this won't actually ever happen + # since if the algorithm is really unknown we'd already have + # raised an exception above + raise ValidationFailure('unknown algorithm %u' % rrsig.algorithm) + # If we got here, we successfully verified so we can return without error return + except ValueError: + # this happens on an individual validation failure + continue + # nothing verified -- raise failure: raise ValidationFailure('verify failure') @@ -444,10 +453,8 @@ def _validate(rrset, rrsigset, keys, origin=None, now=None): rrname = rrset.name if isinstance(rrsigset, tuple): - rrsigname = rrsigset[0] rrsigrdataset = rrsigset[1] else: - rrsigname = rrsigset.name rrsigrdataset = rrsigset rrname = rrname.choose_relativity(origin) @@ -465,7 +472,7 @@ def _validate(rrset, rrsigset, keys, origin=None, now=None): def _need_pycrypto(*args, **kwargs): - raise NotImplementedError("DNSSEC validation requires pycrypto") + raise NotImplementedError("DNSSEC validation requires pycryptodome") try: import Crypto.PublicKey.RSA diff --git a/dns/hash.py b/dns/hash.py deleted file mode 100644 index 966838a..0000000 --- a/dns/hash.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (C) 2011 Nominum, Inc. -# -# Permission to use, copy, modify, and distribute this software and its -# documentation for any purpose with or without fee is hereby granted, -# provided that the above copyright notice and this permission notice -# appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT -# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -"""Hashing backwards compatibility wrapper""" - -import hashlib - - -hashes = {} -hashes['MD5'] = hashlib.md5 -hashes['SHA1'] = hashlib.sha1 -hashes['SHA224'] = hashlib.sha224 -hashes['SHA256'] = hashlib.sha256 -hashes['SHA384'] = hashlib.sha384 -hashes['SHA512'] = hashlib.sha512 - - -def get(algorithm): - return hashes[algorithm.upper()] diff --git a/dns/tsig.py b/dns/tsig.py index c57d879..fd9d56a 100644 --- a/dns/tsig.py +++ b/dns/tsig.py @@ -19,9 +19,9 @@ import struct import dns.exception -import dns.hash import dns.rdataclass import dns.name +import dns.dnssec from ._compat import long, string_types, text_type class BadTime(dns.exception.DNSException): @@ -211,7 +211,7 @@ def get_algorithm(algorithm): algorithm = dns.name.from_text(algorithm) try: - return (algorithm.to_digestable(), dns.hash.hashes[_hashes[algorithm]]) + return (algorithm.to_digestable(), dns.dnssec._make_hash(algorithm)) except KeyError: raise NotImplementedError("TSIG algorithm " + str(algorithm) + " is not supported") diff --git a/doc/dnssec.rst b/doc/dnssec.rst index c16a618..64f08b3 100644 --- a/doc/dnssec.rst +++ b/doc/dnssec.rst @@ -4,11 +4,10 @@ DNSSEC ====== -Dnspython can do simple DNSSEC signature validation, but -currently has no facilities for signing. In order to -use DNSSEC functions, you must have ``pycrypto`` installed. -If you want to do elliptic curves, you must also have -``ecdsa`` installed. +Dnspython can do simple DNSSEC signature validation, but currently has no +facilities for signing. In order to use DNSSEC functions, you must have +``pycryptodome`` installed. If you want to do elliptic curves, you must also +have ``ecdsa`` installed. DNSSEC Algorithms ----------------- diff --git a/doc/installation.rst b/doc/installation.rst index d5c6634..7854f3d 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -45,8 +45,8 @@ Optional Modules The following modules are optional, but recommended for full functionality. -If ``pycrypto`` is installed, then dnspython will be able to do -low-level DNSSEC RSA and DSA signature validation. +If ``pycryptodome`` is installed, then dnspython will be able to do low-level +DNSSEC RSA and DSA signature validation. If ``ecdsa`` is installed, then Elliptic Curve signature algorithms will be available for low-level DNSSEC signature validation. diff --git a/tests/test_dnssec.py b/tests/test_dnssec.py index 80bd626..9fb037e 100644 --- a/tests/test_dnssec.py +++ b/tests/test_dnssec.py @@ -156,22 +156,22 @@ abs_ecdsa384_soa_rrsig = dns.rrset.from_text('example.', 86400, 'IN', 'RRSIG', "SOA 14 1 86400 20130929021229 20130921230729 63571 example. CrnCu34EeeRz0fEhL9PLlwjpBKGYW8QjBjFQTwd+ViVLRAS8tNkcDwQE NhSV89NEjj7ze1a/JcCfcJ+/mZgnvH4NHLNg3Tf6KuLZsgs2I4kKQXEk 37oIHravPEOlGYNI") -@unittest.skipUnless(import_ok, "skipping DNSSEC tests because pycrypto is not" +@unittest.skipUnless(import_ok, "skipping DNSSEC tests because pycryptodome is not" " installed") class DNSSECValidatorTestCase(unittest.TestCase): @unittest.skipUnless(dns.dnssec._have_pycrypto, - "PyCrypto cannot be imported") + "Pycryptodome cannot be imported") def testAbsoluteRSAGood(self): dns.dnssec.validate(abs_soa, abs_soa_rrsig, abs_keys, None, when) @unittest.skipUnless(dns.dnssec._have_pycrypto, - "PyCrypto cannot be imported") + "Pycryptodome cannot be imported") def testDuplicateKeytag(self): dns.dnssec.validate(abs_soa, abs_soa_rrsig, abs_keys_duplicate_keytag, None, when) @unittest.skipUnless(dns.dnssec._have_pycrypto, - "PyCrypto cannot be imported") + "Pycryptodome cannot be imported") def testAbsoluteRSABad(self): def bad(): dns.dnssec.validate(abs_other_soa, abs_soa_rrsig, abs_keys, None, @@ -179,13 +179,13 @@ def bad(): self.failUnlessRaises(dns.dnssec.ValidationFailure, bad) @unittest.skipUnless(dns.dnssec._have_pycrypto, - "PyCrypto cannot be imported") + "Pycryptodome cannot be imported") def testRelativeRSAGood(self): dns.dnssec.validate(rel_soa, rel_soa_rrsig, rel_keys, abs_dnspython_org, when) @unittest.skipUnless(dns.dnssec._have_pycrypto, - "PyCrypto cannot be imported") + "Pycryptodome cannot be imported") def testRelativeRSABad(self): def bad(): dns.dnssec.validate(rel_other_soa, rel_soa_rrsig, rel_keys, @@ -197,13 +197,13 @@ def testMakeSHA256DS(self): self.failUnless(ds == good_ds) @unittest.skipUnless(dns.dnssec._have_pycrypto, - "PyCrypto cannot be imported") + "Pycryptodome cannot be imported") def testAbsoluteDSAGood(self): dns.dnssec.validate(abs_dsa_soa, abs_dsa_soa_rrsig, abs_dsa_keys, None, when2) @unittest.skipUnless(dns.dnssec._have_pycrypto, - "PyCrypto cannot be imported") + "Pycryptodome cannot be imported") def testAbsoluteDSABad(self): def bad(): dns.dnssec.validate(abs_other_dsa_soa, abs_dsa_soa_rrsig, From c5534e037b0f3aa552feaab157ff3fb9496ff821 Mon Sep 17 00:00:00 2001 From: Daniel Robbins Date: Thu, 21 Dec 2017 12:19:07 -0700 Subject: [PATCH 2/2] update travis to use pycryptodome --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5bd79bc..b1a5590 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ branches: except: - python3 install: - - pip install unittest2 pylint pycrypto ecdsa idna + - pip install unittest2 pylint pycryptodome ecdsa idna script: - if [[ ($TRAVIS_PYTHON_VERSION != '2.6') ]]; then make lint; fi - make test