]> git.ipfire.org Git - thirdparty/dnspython.git/commitdiff
Update DNSSEC code to use pycryptodome instead of pycrypto. These changes
authorDaniel Robbins <drobbins@funtoo.org>
Thu, 21 Dec 2017 16:24:40 +0000 (09:24 -0700)
committerTomas Krizek <tomas.krizek@nic.cz>
Fri, 20 Jul 2018 14:24:14 +0000 (16:24 +0200)
make dnspython *incompatible* with pycrypto -- pycryptodome must be used.
The ecdsa module continues to be used for ECDSA support.

ChangeLog
dns/__init__.py
dns/dnssec.py
dns/hash.py [deleted file]
dns/tsig.py
doc/dnssec.rst
doc/installation.rst
tests/test_dnssec.py

index a8fe0b2a34d9b06fb2fa62ff6a863a72f04c18e6..1e15697bfc83b913da359fa19a4aa17d99d19fdf 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,8 @@
+2017-12-21  Daniel Robbins  <drobbins@funtoo.org>
+
+       * dns/dnssec.py: migrated code from pycrypto (apparently no
+         longer maintained) to pycryptodome. All tests passing.
+
 2018-07-18  Tomas Krizek  <tomas.krizek@nic.cz>
 
        * Support for obsolete Python 2.6 and 3.3 (both EOL) dropped.
index c848e485813bb14badba05b2449436adbd85b1ee..3852729b71d6854dea29b0eabe1b90d4d438947a 100644 (file)
@@ -22,7 +22,6 @@ __all__ = [
     'entropy',
     'exception',
     'flags',
-    'hash',
     'inet',
     'ipv4',
     'ipv6',
index 0791226199a2076cfd6d98f5a2129277a269b0eb..1e8b05f4d5fd9d16274c74f18c03c3df63ed1b32 100644 (file)
@@ -20,7 +20,6 @@ import struct
 import time
 
 import dns.exception
-import dns.hash
 import dns.name
 import dns.node
 import dns.rdataset
@@ -28,7 +27,8 @@ import dns.rdata
 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 @@ PRIVATEDNS = 253
 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 (file)
index 966838a..0000000
+++ /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()]
index c57d879fa6c9fbab9069d5b656bb00f8b13a7c41..fd9d56a3e6cd6cf28538a8a00958b2f0afbb10dc 100644 (file)
@@ -19,9 +19,9 @@ import hmac
 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")
index c16a61865f8d01f163d41c2ec6e41ab662166b56..64f08b3c932ff005e7bffd604bafab2502fe7947 100644 (file)
@@ -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
 -----------------
index d5c6634e9964520347af3eb4e07bad3ce9485ca2..7854f3daac1cb22ff5d326cb11e948357eb4d589 100644 (file)
@@ -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.
index 80bd626908204b8e001ffe74c62cb59c3c4135a9..9fb037e1721ef3918c1aa545ff8803b1c2ea1fc2 100644 (file)
@@ -156,22 +156,22 @@ abs_other_ecdsa384_soa = dns.rrset.from_text('example.', 86400, 'IN', 'SOA',
 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 @@ class DNSSECValidatorTestCase(unittest.TestCase):
         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 @@ class DNSSECValidatorTestCase(unittest.TestCase):
         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,