diff options
author | Brian Wellington <bwelling@xbill.org> | 2020-03-23 12:13:20 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-23 12:13:20 -0700 |
commit | 665f8b0cedbf1a416b29664abd4ba1e1f28a789a (patch) | |
tree | ccfaa772fac8fd7cc442f31d3d22f237a02941d6 | |
parent | af1689ab2c596665e6ccd792fe541932c61c1fe4 (diff) | |
parent | 90b89ad829989cccc7c3773505c9798c1f915fa9 (diff) | |
download | dnspython-665f8b0cedbf1a416b29664abd4ba1e1f28a789a.tar.gz |
Merge pull request #433 from fabian-hk/feature/nsec3-hash
NSEC3 hash
-rw-r--r-- | dns/dnssec.py | 42 | ||||
-rw-r--r-- | dns/dnssec.pyi | 3 | ||||
-rw-r--r-- | tests/test_nsec3_hash.py | 56 |
3 files changed, 101 insertions, 0 deletions
diff --git a/dns/dnssec.py b/dns/dnssec.py index 055e47a..137b9aa 100644 --- a/dns/dnssec.py +++ b/dns/dnssec.py @@ -22,6 +22,7 @@ from io import BytesIO import struct import sys import time +import base64 import dns.exception import dns.name @@ -519,6 +520,47 @@ def _validate(rrset, rrsigset, keys, origin=None, now=None): raise ValidationFailure("no RRSIGs validated") +def nsec3_hash(domain, salt, iterations, algo): + """ + This method calculates the NSEC3 hash after: https://tools.ietf.org/html/rfc5155#section-5 + + :param domain: + :type domain: str + :param salt: + :type salt: Optional[str, bytes] + :param iterations: + :type iterations: int + :param algo: + :type algo: int + :return: NSEC3 hash + :rtype: str + """ + b32_conversion = str.maketrans( + "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", "0123456789ABCDEFGHIJKLMNOPQRSTUV" + ) + + if algo != 1: + raise ValueError("Wrong hash algorithm (only SHA1 is supported)") + + salt_encoded = salt + if isinstance(salt, str): + if len(salt) % 2 == 0: + salt_encoded = bytes.fromhex(salt) + else: + raise ValueError("Invalid salt length") + + domain_encoded = dns.name.from_text(domain).canonicalize().to_wire() + + digest = hashlib.sha1(domain_encoded + salt_encoded).digest() + for i in range(iterations): + digest = hashlib.sha1(digest + salt_encoded).digest() + + output = base64.b32encode(digest).decode("utf-8") + output = output.translate(b32_conversion) + + return output + + def _need_pycrypto(*args, **kwargs): raise ImportError("DNSSEC validation requires pycryptodome/pycryptodomex") diff --git a/dns/dnssec.pyi b/dns/dnssec.pyi index da02c15..8902747 100644 --- a/dns/dnssec.pyi +++ b/dns/dnssec.pyi @@ -16,3 +16,6 @@ class ValidationFailure(exception.DNSException): def make_ds(name : name.Name, key : DNSKEY.DNSKEY, algorithm : str, origin : Optional[name.Name] = None) -> DS.DS: ... + +def nsec3_hash(domain: str, salt: Optional[str, bytes], iterations: int, algo: int) -> str: + ... diff --git a/tests/test_nsec3_hash.py b/tests/test_nsec3_hash.py new file mode 100644 index 0000000..f2a3a7a --- /dev/null +++ b/tests/test_nsec3_hash.py @@ -0,0 +1,56 @@ +import unittest + +from dns import dnssec + + +class NSEC3Hash(unittest.TestCase): + + DATA = [ + # Source: https://tools.ietf.org/html/rfc5155#appendix-A + ("example", "aabbccdd", 12, "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom", 1), + ("a.example", "aabbccdd", 12, "35mthgpgcu1qg68fab165klnsnk3dpvl", 1), + ("ai.example", "aabbccdd", 12, "gjeqe526plbf1g8mklp59enfd789njgi", 1), + ("ns1.example", "aabbccdd", 12, "2t7b4g4vsa5smi47k61mv5bv1a22bojr", 1), + ("ns2.example", "aabbccdd", 12, "q04jkcevqvmu85r014c7dkba38o0ji5r", 1), + ("w.example", "aabbccdd", 12, "k8udemvp1j2f7eg6jebps17vp3n8i58h", 1), + ("*.w.example", "aabbccdd", 12, "r53bq7cc2uvmubfu5ocmm6pers9tk9en", 1), + ("x.w.example", "aabbccdd", 12, "b4um86eghhds6nea196smvmlo4ors995", 1), + ("y.w.example", "aabbccdd", 12, "ji6neoaepv8b5o6k4ev33abha8ht9fgc", 1), + ("x.y.w.example", "aabbccdd", 12, "2vptu5timamqttgl4luu9kg21e0aor3s", 1), + ("xx.example", "aabbccdd", 12, "t644ebqk9bibcna874givr6joj62mlhv", 1), + ( + "2t7b4g4vsa5smi47k61mv5bv1a22bojr.example", + "aabbccdd", + 12, + "kohar7mbb8dc2ce8a9qvl8hon4k53uhi", + 1, + ), + # Source: generated with knsec3hash (Linux knot package) + ("example.com", "9F1AB450CF71D6", 0, "qfo2sv6jaej4cm11a3npoorfrckdao2c", 1), + ("example.com", "9F1AB450CF71D6", 1, "1nr64to0bb861lku97deb4ubbk6cl5qh", 1), + ("example.com.", "AF6AB45CCF79D6", 6, "sale3fn6penahh1lq5oqtr5rcl1d113a", 1), + ("test.domain.dev.", "", 6, "8q98lv9jgkhoq272e42c8blesivia7bu", 1), + ("www.test.domain.dev.", "B4", 2, "nv7ti6brgh94ke2f3pgiigjevfgpo5j0", 1), + ("*.test-domain.dev", "", 0, "o6uadafckb6hea9qpcgir2gl71vt23gu", 1), + ("*.test-domain.dev", "", 45, "505k9g118d9sofnjhh54rr8fadgpa0ct", 1), + ] + + def test_hash_function(self): + for d in self.DATA: + hash = dnssec.nsec3_hash(d[0], d[1], d[2], d[4]) + self.assertEqual(hash, d[3].upper(), f"Error {d}") + + def test_hash_invalid_salt_length(self): + data = ( + "example.com", + "9F1AB450CF71D", + 0, + "qfo2sv6jaej4cm11a3npoorfrckdao2c", + 1, + ) + with self.assertRaises(ValueError): + hash = dnssec.nsec3_hash(data[0], data[1], data[2], data[4]) + + +if __name__ == "__main__": + unittest.main() |