]> git.ipfire.org Git - thirdparty/dnspython.git/commitdiff
NSEC3 hash implementation and tests 433/head
authorFabian Hauck <hauckfabian@gmail.com>
Sat, 21 Mar 2020 13:56:27 +0000 (14:56 +0100)
committerFabian Hauck <hauckfabian@gmail.com>
Sat, 21 Mar 2020 13:56:27 +0000 (14:56 +0100)
dns/dnssec.py
dns/dnssec.pyi
tests/test_nsec3_hash.py [new file with mode: 0644]

index 055e47ad6101911bd50cae7ae3de20c1b8ea7f99..137b9aa68ac38a5f01e9a3268722d92813b1ec8c 100644 (file)
@@ -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")
 
index da02c1510f613c20d088c5d4d9ab74bf3620ffaa..89027477f3cfb06d440ce2ee2d269cdee8796f12 100644 (file)
@@ -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 (file)
index 0000000..f2a3a7a
--- /dev/null
@@ -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()