From: Brian Wellington Date: Wed, 11 Mar 2020 16:31:22 +0000 (-0700) Subject: Add support for EdDSA DNSSEC algorithms. X-Git-Tag: v2.0.0rc1~330^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f1245d42704b70bf17813ac2b22a517f577af1ae;p=thirdparty%2Fdnspython.git Add support for EdDSA DNSSEC algorithms. --- diff --git a/.travis.yml b/.travis.yml index 61c95220..99aad5c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/dns/dnssec.py b/dns/dnssec.py index f2b453c7..1bc840c5 100644 --- a/dns/dnssec.py +++ b/dns/dnssec.py @@ -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 diff --git a/doc/dnssec.rst b/doc/dnssec.rst index 079ba231..81abc311 100644 --- a/doc/dnssec.rst +++ b/doc/dnssec.rst @@ -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 ---------------- diff --git a/doc/installation.rst b/doc/installation.rst index 9c619e90..63f29ff0 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -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. diff --git a/setup.py b/setup.py index 2dac565e..436bd138 100755 --- 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, diff --git a/tests/test_dnssec.py b/tests/test_dnssec.py index 31078c7b..223f8d42 100644 --- a/tests/test_dnssec.py +++ b/tests/test_dnssec.py @@ -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):