]> git.ipfire.org Git - thirdparty/dnspython.git/commitdiff
Add support for EdDSA DNSSEC algorithms.
authorBrian Wellington <bwelling@xbill.org>
Wed, 11 Mar 2020 16:31:22 +0000 (09:31 -0700)
committerBrian Wellington <bwelling@xbill.org>
Wed, 11 Mar 2020 16:31:22 +0000 (09:31 -0700)
.travis.yml
dns/dnssec.py
doc/dnssec.rst
doc/installation.rst
setup.py
tests/test_dnssec.py

index 61c9522074ed4c7a4e3d9c2020f997091b915d4b..99aad5c4b2bf6c481475da5bd19c638bfbbfc243 100644 (file)
@@ -16,6 +16,6 @@ branches:
   except:
     - python3
 install:
- - pip install typing pylint pycryptodome idna requests requests-toolbelt
+ - pip install typing pylint pycryptodome ecpy idna requests requests-toolbelt
 script:
  - make test
index f2b453c70b03613172e29978bf4a1c210d51839c..1bc840c5513949a8a569942410fe46da7f13f9ee 100644 (file)
@@ -261,6 +261,11 @@ def _is_sha384(algorithm):
 def _is_sha512(algorithm):
     return algorithm == RSASHA512
 
+class _IdentityHasher:
+    def __init__(self):
+        self.value = b''
+    def update(self, s):
+        self.value += s
 
 def _make_hash(algorithm):
     if _is_md5(algorithm):
@@ -273,6 +278,9 @@ def _make_hash(algorithm):
         return SHA384.new()
     if _is_sha512(algorithm):
         return SHA512.new()
+    if _is_eddsa(algorithm):
+        return _IdentityHasher()
+
     raise ValidationFailure('unknown hash for algorithm %u' % algorithm)
 
 
@@ -377,7 +385,18 @@ def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None):
                 point_y = number.bytes_to_long(ecdsa_y))
             sig = rrsig.signature
 
-        elif _is_eddsa(rrsig.algorithm) or _is_gost(rrsig.algorithm):
+        elif _is_eddsa(rrsig.algorithm):
+            keyptr = candidate_key.key
+            if not _have_ecpy:
+                raise ImportError('DNSSEC validation for algorithm %u requires ecpy library' % rrsig.algorithm)
+            if rrsig.algorithm == ED25519:
+                curve = 'Ed25519'
+            else:
+                curve = 'Ed448'
+            point = Curve.get_curve(curve).decode_point(keyptr)
+            pubkey = ECPublicKey(point)
+            sig = rrsig.signature
+        elif _is_gost(rrsig.algorithm):
             raise UnsupportedAlgorithm(
                 'algorithm "%s" not supported by dnspython' % algorithm_to_text(rrsig.algorithm))
         else:
@@ -410,6 +429,13 @@ def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None):
             elif _is_dsa(rrsig.algorithm) or _is_ecdsa(rrsig.algorithm):
                 verifier = DSS.new(pubkey, 'fips-186-3')
                 verifier.verify(hash, sig)
+            elif _is_eddsa(rrsig.algorithm):
+                if rrsig.algorithm == ED25519:
+                    verifier = EDDSA(hashlib.sha512)
+                else:
+                    verifier = EDDSA(hashlib.shake_256, 114)
+                if not verifier.verify(hash.value, sig, pubkey):
+                    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
@@ -497,3 +523,12 @@ else:
     validate = _validate
     validate_rrsig = _validate_rrsig
     _have_pycrypto = True
+
+    try:
+        from ecpy.curves import Curve, Point
+        from ecpy.keys import ECPublicKey
+        from ecpy.eddsa import EDDSA
+    except ImportError:
+        _have_ecpy = False
+    else:
+        _have_ecpy = True
index 079ba2315eb3ea237db09d8e08eab30f21e57bb1..81abc311828929eb0a3274c1f57ac5db11b60f50 100644 (file)
@@ -6,7 +6,8 @@ DNSSEC
 
 Dnspython can do simple DNSSEC signature validation, but currently has no
 facilities for signing.  In order to use DNSSEC functions, you must have
-``pycryptodome`` or ``pycryptodomex`` installed.
+``pycryptodome`` or ``pycryptodomex`` installed.  In order to use the EdDSA
+algorithms, you must also have ``ecpy`` installed.
 
 DNSSEC Functions
 ----------------
index 9c619e90bc34c183345b0bfd8d7f7e95a90d6959..63f29ff0a0fb21809779182f69914936a49e053f 100644 (file)
@@ -48,4 +48,7 @@ The following modules are optional, but recommended for full functionality.
 If ``pycryptodome`` / ``pycryptodomex`` is installed, then dnspython will be
 able to do low-level DNSSEC RSA, DSA, and ECDSA signature validation.
 
+If ``ecpy`` is installed as well, then dnspython will be able to do low-level
+EdDSA signature verification.
+
 If ``idna`` is installed, then IDNA 2008 will be available.
index 2dac565ef674efcc1fa30049104da0d7314d0281..436bd13853a6fdb02fdece52091f3318b75b850b 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -75,7 +75,7 @@ direct manipulation of DNS zones, messages, names, and records.""",
     'tests_require': ['typing ; python_version<"3.5"'],
     'extras_require': {
         'IDNA': ['idna>=2.1'],
-        'DNSSEC': ['pycryptodome>=3.4'],
+        'DNSSEC': ['pycryptodome>=3.4', 'ecpy'],
         },
     'ext_modules': ext_modules if compile_cython else None,
     'zip_safe': False if compile_cython else None,
index 31078c7bd4598720bd53edd6cf5e54ccecbc251e..223f8d428e1781d12d948540dcf673445ce31113 100644 (file)
@@ -149,6 +149,49 @@ 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")
 
+abs_example_com = dns.name.from_text('example.com')
+
+abs_ed25519_mx = dns.rrset.from_text('example.com.', 3600, 'IN', 'MX',
+                                     '10 mail.example.com.')
+abs_other_ed25519_mx = dns.rrset.from_text('example.com.', 3600, 'IN', 'MX',
+                                           '11 mail.example.com.')
+abs_ed25519_keys_1 = {
+    abs_example_com: dns.rrset.from_text(
+        'example.com', 3600, 'IN', 'DNSKEY',
+        '257 3 15 l02Woi0iS8Aa25FQkUd9RMzZHJpBoRQwAQEX1SxZJA4=')
+}
+abs_ed25519_mx_rrsig_1 = dns.rrset.from_text('example.com.', 3600, 'IN', 'RRSIG',
+                                             'MX 15 2 3600 1440021600 1438207200 3613 example.com. oL9krJun7xfBOIWcGHi7mag5/hdZrKWw15jPGrHpjQeRAvTdszaPD+QLs3fx8A4M3e23mRZ9VrbpMngwcrqNAg==')
+
+abs_ed25519_keys_2 = {
+    abs_example_com: dns.rrset.from_text(
+        'example.com', 3600, 'IN', 'DNSKEY',
+        '257 3 15 zPnZ/QwEe7S8C5SPz2OfS5RR40ATk2/rYnE9xHIEijs=')
+}
+abs_ed25519_mx_rrsig_2 = dns.rrset.from_text('example.com.', 3600, 'IN', 'RRSIG',
+                                             'MX 15 2 3600 1440021600 1438207200 35217 example.com. zXQ0bkYgQTEFyfLyi9QoiY6D8ZdYo4wyUhVioYZXFdT410QPRITQSqJSnzQoSm5poJ7gD7AQR0O7KuI5k2pcBg==')
+
+abs_ed448_mx = abs_ed25519_mx
+abs_other_ed448_mx = abs_other_ed25519_mx
+
+abs_ed448_keys_1 = {
+    abs_example_com: dns.rrset.from_text(
+        'example.com', 3600, 'IN', 'DNSKEY',
+        '257 3 16 3kgROaDjrh0H2iuixWBrc8g2EpBBLCdGzHmn+G2MpTPhpj/OiBVHHSfPodx1FYYUcJKm1MDpJtIA')
+}
+abs_ed448_mx_rrsig_1 = dns.rrset.from_text('example.com.', 3600, 'IN', 'RRSIG',
+                                           'MX 16 2 3600 1440021600 1438207200 9713 example.com. 3cPAHkmlnxcDHMyg7vFC34l0blBhuG1qpwLmjInI8w1CMB29FkEAIJUA0amxWndkmnBZ6SKiwZSAxGILn/NBtOXft0+Gj7FSvOKxE/07+4RQvE581N3Aj/JtIyaiYVdnYtyMWbSNyGEY2213WKsJlwEA')
+
+abs_ed448_keys_2 = {
+    abs_example_com: dns.rrset.from_text(
+        'example.com', 3600, 'IN', 'DNSKEY',
+        '257 3 16 kkreGWoccSDmUBGAe7+zsbG6ZAFQp+syPmYUurBRQc3tDjeMCJcVMRDmgcNLp5HlHAMy12VoISsA')
+}
+abs_ed448_mx_rrsig_2 = dns.rrset.from_text('example.com.', 3600, 'IN', 'RRSIG',
+                                           'MX 16 2 3600 1440021600 1438207200 38353 example.com. E1/oLjSGIbmLny/4fcgM1z4oL6aqo+izT3urCyHyvEp4Sp8Syg1eI+lJ57CSnZqjJP41O/9l4m0AsQ4f7qI1gVnML8vWWiyW2KXhT9kuAICUSxv5OWbf81Rq7Yu60npabODB0QFPb/rkW3kUZmQ0YQUA')
+
+when5 = 1440021600
+
 
 @unittest.skipUnless(dns.dnssec._have_pycrypto,
                      "Pycryptodome cannot be imported")
@@ -206,6 +249,41 @@ class DNSSECValidatorTestCase(unittest.TestCase):
                                 abs_ecdsa384_keys, None, when4)
         self.assertRaises(dns.dnssec.ValidationFailure, bad)
 
+    @unittest.skipUnless(dns.dnssec._have_ecpy,
+                         "python EDDSA cannot be imported")
+    def testAbsoluteED25519Good(self):  # type: () -> None
+        dns.dnssec.validate(abs_ed25519_mx, abs_ed25519_mx_rrsig_1,
+                            abs_ed25519_keys_1, None, when5)
+        dns.dnssec.validate(abs_ed25519_mx, abs_ed25519_mx_rrsig_2,
+                            abs_ed25519_keys_2, None, when5)
+
+    @unittest.skipUnless(dns.dnssec._have_ecpy,
+                         "python EDDSA cannot be imported")
+    def testAbsoluteED25519Bad(self):  # type: () -> None
+        with self.assertRaises(dns.dnssec.ValidationFailure):
+            dns.dnssec.validate(abs_other_ed25519_mx, abs_ed25519_mx_rrsig_1,
+                                abs_ed25519_keys_1, None, when5)
+        with self.assertRaises(dns.dnssec.ValidationFailure):
+            dns.dnssec.validate(abs_other_ed25519_mx, abs_ed25519_mx_rrsig_2,
+                                abs_ed25519_keys_2, None, when5)
+
+    @unittest.skipUnless(dns.dnssec._have_ecpy,
+                         "python EDDSA cannot be imported")
+    def testAbsoluteED448Good(self):  # type: () -> None
+        dns.dnssec.validate(abs_ed448_mx, abs_ed448_mx_rrsig_1,
+                            abs_ed448_keys_1, None, when5)
+        dns.dnssec.validate(abs_ed448_mx, abs_ed448_mx_rrsig_2,
+                            abs_ed448_keys_2, None, when5)
+
+    @unittest.skipUnless(dns.dnssec._have_ecpy,
+                         "python EDDSA cannot be imported")
+    def testAbsoluteED448Bad(self):  # type: () -> None
+        with self.assertRaises(dns.dnssec.ValidationFailure):
+            dns.dnssec.validate(abs_other_ed448_mx, abs_ed448_mx_rrsig_1,
+                                abs_ed448_keys_1, None, when5)
+        with self.assertRaises(dns.dnssec.ValidationFailure):
+            dns.dnssec.validate(abs_other_ed448_mx, abs_ed448_mx_rrsig_2,
+                                abs_ed448_keys_2, None, when5)
 
 class DNSSECMakeDSTestCase(unittest.TestCase):