summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian Wellington <bwelling@xbill.org>2020-03-23 12:13:20 -0700
committerGitHub <noreply@github.com>2020-03-23 12:13:20 -0700
commit665f8b0cedbf1a416b29664abd4ba1e1f28a789a (patch)
treeccfaa772fac8fd7cc442f31d3d22f237a02941d6
parentaf1689ab2c596665e6ccd792fe541932c61c1fe4 (diff)
parent90b89ad829989cccc7c3773505c9798c1f915fa9 (diff)
downloaddnspython-665f8b0cedbf1a416b29664abd4ba1e1f28a789a.tar.gz
Merge pull request #433 from fabian-hk/feature/nsec3-hash
NSEC3 hash
-rw-r--r--dns/dnssec.py42
-rw-r--r--dns/dnssec.pyi3
-rw-r--r--tests/test_nsec3_hash.py56
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()