]> git.ipfire.org Git - thirdparty/dnspython.git/commitdiff
Checkpoint ZONEMD support.
authorBrian Wellington <bwelling@xbill.org>
Wed, 24 Feb 2021 18:15:46 +0000 (10:15 -0800)
committerBrian Wellington <bwelling@xbill.org>
Wed, 24 Feb 2021 18:15:46 +0000 (10:15 -0800)
dns/rdatatype.py
dns/rdtypes/ANY/ZONEMD.py [new file with mode: 0644]
dns/rdtypes/ANY/__init__.py
dns/zone.py
tests/example
tests/example1.good
tests/example2.good
tests/example3.good
tests/test_zonedigest.py [new file with mode: 0644]

index 65da6d4ef5b88e5cf9d9c0188ec846efd4b2869b..b7b095c2d5053aec06b106ee3be4c3fb5140a0a4 100644 (file)
@@ -79,6 +79,7 @@ class RdataType(dns.enum.IntEnum):
     CDNSKEY = 60
     OPENPGPKEY = 61
     CSYNC = 62
+    ZONEMD = 63
     SVCB = 64
     HTTPS = 65
     SPF = 99
@@ -280,6 +281,7 @@ CDS = RdataType.CDS
 CDNSKEY = RdataType.CDNSKEY
 OPENPGPKEY = RdataType.OPENPGPKEY
 CSYNC = RdataType.CSYNC
+ZONEMD = RdataType.ZONEMD
 SVCB = RdataType.SVCB
 HTTPS = RdataType.HTTPS
 SPF = RdataType.SPF
diff --git a/dns/rdtypes/ANY/ZONEMD.py b/dns/rdtypes/ANY/ZONEMD.py
new file mode 100644 (file)
index 0000000..0d9eedc
--- /dev/null
@@ -0,0 +1,66 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import hashlib
+import struct
+import binascii
+
+import dns.immutable
+import dns.rdata
+import dns.rdatatype
+import dns.zone
+
+
+@dns.immutable.immutable
+class ZONEMD(dns.rdata.Rdata):
+
+    """ZONEMD record"""
+
+    # See RFC 8976
+
+    __slots__ = ['serial', 'scheme', 'hash_algorithm', 'digest']
+
+    def __init__(self, rdclass, rdtype, serial, scheme, hash_algorithm, digest):
+        super().__init__(rdclass, rdtype)
+        self.serial = self._as_uint32(serial)
+        self.scheme = dns.zone.DigestScheme.make(scheme)
+        self.hash_algorithm = dns.zone.DigestHashAlgorithm.make(hash_algorithm)
+        self.digest = self._as_bytes(digest)
+
+        if self.scheme == 0:  # reserved, RFC 8976 Sec. 5.2
+            raise ValueError('scheme 0 is reserved')
+        if self.hash_algorithm == 0:  # reserved, RFC 8976 Sec. 5.3
+            raise ValueError('hash_algorithm 0 is reserved')
+
+        hasher = dns.zone._digest_hashers.get(self.hash_algorithm)
+        if hasher and hasher().digest_size != len(self.digest):
+            raise ValueError('digest length inconsistent with hash algorithm')
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        kw = kw.copy()
+        chunksize = kw.pop('chunksize', 128)
+        return '%d %d %d %s' % (self.serial, self.scheme, self.hash_algorithm,
+                                dns.rdata._hexify(self.digest,
+                                                  chunksize=chunksize,
+                                                  **kw))
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        serial = tok.get_uint32()
+        scheme = tok.get_uint8()
+        hash_algorithm = tok.get_uint8()
+        digest = tok.concatenate_remaining_identifiers().encode()
+        digest = binascii.unhexlify(digest)
+        return cls(rdclass, rdtype, serial, scheme, hash_algorithm, digest)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        header = struct.pack("!IBB", self.serial, self.scheme,
+                             self.hash_algorithm)
+        file.write(header)
+        file.write(self.digest)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        header = parser.get_struct("!IBB")
+        digest = parser.get_remaining()
+        return cls(rdclass, rdtype, header[0], header[1], header[2], digest)
index 5d672137217c3f190c65f38cc034a58e8ab7815b..6c56baffdb4122da3083cc78f42c371236242727 100644 (file)
@@ -60,4 +60,5 @@ __all__ = [
     'TXT',
     'URI',
     'X25',
+    'ZONEMD',
 ]
index ac9576386938854b01e407122e5db057c8e30e4d..b9fad66d0695eb9c220af574c8b26025ec17bc30 100644 (file)
 """DNS Zones."""
 
 import contextlib
+import hashlib
 import io
 import os
+import struct
 
 import dns.exception
 import dns.name
@@ -28,6 +30,7 @@ import dns.rdataclass
 import dns.rdatatype
 import dns.rdata
 import dns.rdtypes.ANY.SOA
+import dns.rdtypes.ANY.ZONEMD
 import dns.rrset
 import dns.tokenizer
 import dns.transaction
@@ -56,6 +59,33 @@ class UnknownOrigin(BadZone):
     """The DNS zone's origin is unknown."""
 
 
+class DigestScheme(dns.enum.IntEnum):
+    """ZONEMD Scheme"""
+
+    SIMPLE = 1
+
+    @classmethod
+    def _maximum(cls):
+        return 255
+
+
+class DigestHashAlgorithm(dns.enum.IntEnum):
+    """ZONEMD Hash Algorithm"""
+
+    SHA384 = 1
+    SHA512 = 2
+
+    @classmethod
+    def _maximum(cls):
+        return 255
+
+
+_digest_hashers = {
+    DigestHashAlgorithm.SHA384: hashlib.sha384,
+    DigestHashAlgorithm.SHA512 : hashlib.sha512
+}
+
+
 class Zone(dns.transaction.TransactionManager):
 
     """A DNS zone.
@@ -643,6 +673,53 @@ class Zone(dns.transaction.TransactionManager):
         if self.get_rdataset(name, dns.rdatatype.NS) is None:
             raise NoNS
 
+    def _compute_digest(self, hash_algorithm, scheme=DigestScheme.SIMPLE):
+        hashinfo = _digest_hashers.get(hash_algorithm)
+        if not hashinfo:
+            raise ValueError("unknown digest hash algorithm")
+        if scheme != DigestScheme.SIMPLE:
+            raise ValueError("unknown digest scheme")
+
+        hasher = hashinfo()
+        for (name, node) in sorted(self.items()):
+            rrnamebuf = name.to_digestable(self.origin)
+            for rdataset in sorted(node,
+                                   key=lambda rds: (rds.rdtype, rds.covers)):
+                if name in (self.origin, dns.name.empty) and \
+                   dns.rdatatype.ZONEMD in (rdataset.rdtype, rdataset.covers):
+                    continue
+                rrfixed = struct.pack('!HHI', rdataset.rdtype,
+                                      rdataset.rdclass, rdataset.ttl)
+                for rr in sorted(rdataset):
+                    rrdata = rr.to_digestable(self.origin)
+                    rrlen = struct.pack('!H', len(rrdata))
+                    hasher.update(rrnamebuf + rrfixed + rrlen + rrdata)
+        return hasher.digest()
+
+    def compute_digest(self, hash_algorithm, scheme=DigestScheme.SIMPLE):
+        digest = self._compute_digest(hash_algorithm, scheme)
+        return dns.rdtypes.ANY.ZONEMD.ZONEMD(self.rdclass,
+                                             dns.rdatatype.ZONEMD,
+                                             serial, scheme, hash_algorithm,
+                                             digest)
+
+    def verify_digest(self, zonemd=None):
+        if zonemd:
+            digests = [zonemd]
+        else:
+            digests = self.get_rdataset(self.origin, dns.rdatatype.ZONEMD)
+            if digests is None:
+                raise ValueError("no ZONEMD records found")
+        for digest in digests:
+            try:
+                computed = self._compute_digest(digest.hash_algorithm,
+                                                digest.scheme)
+                if computed == digest.digest:
+                    return
+            except Exception as e:
+                pass
+        raise ValueError("no digests verified")
+
     # TransactionManager methods
 
     def reader(self):
index b9a802bf4694540c49795b4417efdb7ea5a0941e..86af9dd8c12368bd6dca4afb4b77cf8bc21a5df7 100644 (file)
@@ -236,6 +236,10 @@ amtrelay04              AMTRELAY  10 0 2 2001:db8::15
 amtrelay05              AMTRELAY 128 1 3 amtrelays.example.com.
 csync0                 CSYNC 12345 0 A MX RRSIG NSEC TYPE1234
 avc01                  AVC "app-name:WOLFGANG|app-class:OAM|business=yes"
+zonemd01                ZONEMD 2018031900 1 1 62e6cf51b02e54b9 b5f967d547ce4313 6792901f9f88e637 493daaf401c92c27 9dd10f0edb1c56f8 080211f8480ee306
+zonemd02                ZONEMD 2018031900 1 2 08cfa1115c7b948c 4163a901270395ea 226a930cd2cbcf2f a9a5e6eb85f37c8a 4e114d884e66f176 eab121cb02db7d65 2e0cc4827e7a3204 f166b47e5613fd27
+zonemd03                ZONEMD 2018031900 1 240 e2d523f654b9422a 96c5a8f44607bbee
+zonemd04                ZONEMD 2018031900 241 1 e1846540e33a9e41 89792d18d5d131f6 05fc283e aaaaaaaa aaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaa aaaaaaaaaaaaaaa
 svcb01                  SVCB (
 100 foo.com. mandatory="alpn,port" alpn="h2,h3" no-default-alpn port="12345"
 echconfig="abcd" ipv4hint=1.2.3.4,4.3.2.1 ipv6hint=1::2,3::4 key12345="foo"
index 03fd08dc87ac09de96559b6a9fe97737ab7b8956..c1ddfd49b6160e57095e2a8c98eb5dab98649a00 100644 (file)
@@ -147,3 +147,7 @@ wks01 3600 IN WKS 10.0.0.1 6 0 1 2 21 23
 wks02 3600 IN WKS 10.0.0.1 17 0 1 2 53
 wks03 3600 IN WKS 10.0.0.2 6 65535
 x2501 3600 IN X25 "123456789"
+zonemd01 3600 IN ZONEMD 2018031900 1 1 62e6cf51b02e54b9b5f967d547ce43136792901f9f88e637493daaf401c92c279dd10f0edb1c56f8080211f8480ee306
+zonemd02 3600 IN ZONEMD 2018031900 1 2 08cfa1115c7b948c4163a901270395ea226a930cd2cbcf2fa9a5e6eb85f37c8a4e114d884e66f176eab121cb02db7d652e0cc4827e7a3204f166b47e5613fd27
+zonemd03 3600 IN ZONEMD 2018031900 1 240 e2d523f654b9422a96c5a8f44607bbee
+zonemd04 3600 IN ZONEMD 2018031900 241 1 e1846540e33a9e4189792d18d5d131f605fc283eaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
index 64696ce9e720a75d0093b5d680708b296ff7c615..ac14e202c3eb5a9925bb74bd07809cc0e7aa603b 100644 (file)
@@ -147,3 +147,7 @@ wks01.example. 3600 IN WKS 10.0.0.1 6 0 1 2 21 23
 wks02.example. 3600 IN WKS 10.0.0.1 17 0 1 2 53
 wks03.example. 3600 IN WKS 10.0.0.2 6 65535
 x2501.example. 3600 IN X25 "123456789"
+zonemd01.example. 3600 IN ZONEMD 2018031900 1 1 62e6cf51b02e54b9b5f967d547ce43136792901f9f88e637493daaf401c92c279dd10f0edb1c56f8080211f8480ee306
+zonemd02.example. 3600 IN ZONEMD 2018031900 1 2 08cfa1115c7b948c4163a901270395ea226a930cd2cbcf2fa9a5e6eb85f37c8a4e114d884e66f176eab121cb02db7d652e0cc4827e7a3204f166b47e5613fd27
+zonemd03.example. 3600 IN ZONEMD 2018031900 1 240 e2d523f654b9422a96c5a8f44607bbee
+zonemd04.example. 3600 IN ZONEMD 2018031900 241 1 e1846540e33a9e4189792d18d5d131f605fc283eaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
index 03fd08dc87ac09de96559b6a9fe97737ab7b8956..c1ddfd49b6160e57095e2a8c98eb5dab98649a00 100644 (file)
@@ -147,3 +147,7 @@ wks01 3600 IN WKS 10.0.0.1 6 0 1 2 21 23
 wks02 3600 IN WKS 10.0.0.1 17 0 1 2 53
 wks03 3600 IN WKS 10.0.0.2 6 65535
 x2501 3600 IN X25 "123456789"
+zonemd01 3600 IN ZONEMD 2018031900 1 1 62e6cf51b02e54b9b5f967d547ce43136792901f9f88e637493daaf401c92c279dd10f0edb1c56f8080211f8480ee306
+zonemd02 3600 IN ZONEMD 2018031900 1 2 08cfa1115c7b948c4163a901270395ea226a930cd2cbcf2fa9a5e6eb85f37c8a4e114d884e66f176eab121cb02db7d652e0cc4827e7a3204f166b47e5613fd27
+zonemd03 3600 IN ZONEMD 2018031900 1 240 e2d523f654b9422a96c5a8f44607bbee
+zonemd04 3600 IN ZONEMD 2018031900 241 1 e1846540e33a9e4189792d18d5d131f605fc283eaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
diff --git a/tests/test_zonedigest.py b/tests/test_zonedigest.py
new file mode 100644 (file)
index 0000000..47629c5
--- /dev/null
@@ -0,0 +1,147 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import io
+import textwrap
+import unittest
+
+import dns.rdata
+import dns.rrset
+import dns.zone
+
+class ZoneDigestTestCase(unittest.TestCase):
+    # Examples from RFC 8976, fixed per errata.
+
+    simple_example = textwrap.dedent('''
+        example.      86400  IN  SOA     ns1 admin 2018031900 (
+                                         1800 900 604800 86400 )
+                      86400  IN  NS      ns1
+                      86400  IN  NS      ns2
+                      86400  IN  ZONEMD  2018031900 1 1 (
+                                         c68090d90a7aed71
+                                         6bc459f9340e3d7c
+                                         1370d4d24b7e2fc3
+                                         a1ddc0b9a87153b9
+                                         a9713b3c9ae5cc27
+                                         777f98b8e730044c )
+        ns1           3600   IN  A       203.0.113.63
+        ns2           3600   IN  AAAA    2001:db8::63
+    ''')
+
+    complex_example = textwrap.dedent('''
+        example.      86400  IN  SOA     ns1 admin 2018031900 (
+                                         1800 900 604800 86400 )
+                      86400  IN  NS      ns1
+                      86400  IN  NS      ns2
+                      86400  IN  ZONEMD  2018031900 1 1 (
+                                         a3b69bad980a3504
+                                         e1cffcb0fd6397f9
+                                         3848071c93151f55
+                                         2ae2f6b1711d4bd2
+                                         d8b39808226d7b9d
+                                         b71e34b72077f8fe )
+        ns1           3600   IN  A       203.0.113.63
+        NS2           3600   IN  AAAA    2001:db8::63
+        occluded.sub  7200   IN  TXT     "I'm occluded but must be digested"
+        sub           7200   IN  NS      ns1
+        duplicate     300    IN  TXT     "I must be digested just once"
+        duplicate     300    IN  TXT     "I must be digested just once"
+        foo.test.     555    IN  TXT     "out-of-zone data must be excluded"
+        UPPERCASE     3600   IN  TXT     "canonicalize uppercase owner names"
+        *             777    IN  PTR     dont-forget-about-wildcards
+        mail          3600   IN  MX      20 MAIL1
+        mail          3600   IN  MX      10 Mail2.Example.
+        sortme        3600   IN  AAAA    2001:db8::5:61
+        sortme        3600   IN  AAAA    2001:db8::3:62
+        sortme        3600   IN  AAAA    2001:db8::4:63
+        sortme        3600   IN  AAAA    2001:db8::1:65
+        sortme        3600   IN  AAAA    2001:db8::2:64
+        non-apex      900    IN  ZONEMD  2018031900 1 1 (
+                                         616c6c6f77656420
+                                         6275742069676e6f
+                                         7265642e20616c6c
+                                         6f77656420627574
+                                         2069676e6f726564
+                                         2e20616c6c6f7765 )
+    ''')
+
+    multiple_digests_example = textwrap.dedent('''
+        example.      86400  IN  SOA     ns1 admin 2018031900 (
+                                         1800 900 604800 86400 )
+        example.      86400  IN  NS      ns1.example.
+        example.      86400  IN  NS      ns2.example.
+        example.      86400  IN  ZONEMD  2018031900 1 1 (
+                                         62e6cf51b02e54b9
+                                         b5f967d547ce4313
+                                         6792901f9f88e637
+                                         493daaf401c92c27
+                                         9dd10f0edb1c56f8
+                                         080211f8480ee306 )
+        example.      86400  IN  ZONEMD  2018031900 1 2 (
+                                         08cfa1115c7b948c
+                                         4163a901270395ea
+                                         226a930cd2cbcf2f
+                                         a9a5e6eb85f37c8a
+                                         4e114d884e66f176
+                                         eab121cb02db7d65
+                                         2e0cc4827e7a3204
+                                         f166b47e5613fd27 )
+        example.      86400  IN  ZONEMD  2018031900 1 240 (
+                                         e2d523f654b9422a
+                                         96c5a8f44607bbee )
+        example.      86400  IN  ZONEMD  2018031900 241 1 (
+                                         e1846540e33a9e41
+                                         89792d18d5d131f6
+                                         05fc283eaaaaaaaa
+                                         aaaaaaaaaaaaaaaa
+                                         aaaaaaaaaaaaaaaa
+                                         aaaaaaaaaaaaaaaa)
+        ns1.example.  3600   IN  A       203.0.113.63
+        ns2.example.  86400  IN  TXT     "This example has multiple digests"
+        NS2.EXAMPLE.  3600   IN  AAAA    2001:db8::63
+    ''')
+
+    def test_zonemd_simple(self):
+        zone = dns.zone.from_text(self.simple_example, origin='example')
+        zone.verify_digest()
+
+    def test_zonemd_complex(self):
+        zone = dns.zone.from_text(self.complex_example, origin='example')
+        zone.verify_digest()
+
+    def test_zonemd_multiple_digests(self):
+        zone = dns.zone.from_text(self.multiple_digests_example,
+                                  origin='example')
+        zone.verify_digest()
+
+        zonemd = zone.get_rdataset(zone.origin, 'ZONEMD')
+        for rr in zonemd:
+            if rr.scheme == 1 and rr.hash_algorithm in (1, 2):
+                zone.verify_digest(rr)
+            else:
+                with self.assertRaises(ValueError):
+                    zone.verify_digest(rr)
+
+    sha384_hash = 'ab' * 48
+    sha512_hash = 'ab' * 64
+
+    def test_zonemd_parse_rdata(self):
+        dns.rdata.from_text('IN', 'ZONEMD', '100 1 1 ' + self.sha384_hash)
+        dns.rdata.from_text('IN', 'ZONEMD', '100 1 2 ' + self.sha512_hash)
+        dns.rdata.from_text('IN', 'ZONEMD', '100 100 1 ' + self.sha384_hash)
+        dns.rdata.from_text('IN', 'ZONEMD', '100 1 100 abcd')
+
+    def test_zonemd_unknown_scheme(self):
+        zone = dns.zone.from_text(self.simple_example, origin='example')
+        with self.assertRaises(ValueError):
+            zone.compute_digest(dns.zone.DigestHashAlgorithm.SHA384, 2)
+
+    def test_zonemd_unknown_hash_algorithm(self):
+        zone = dns.zone.from_text(self.simple_example, origin='example')
+        with self.assertRaises(ValueError):
+            zone.compute_digest(5)
+
+    def test_zonemd_invalid_digest_length(self):
+        with self.assertRaises(dns.exception.SyntaxError):
+            dns.rdata.from_text('IN', 'ZONEMD', '100 1 2 ' + self.sha384_hash)
+        with self.assertRaises(dns.exception.SyntaxError):
+            dns.rdata.from_text('IN', 'ZONEMD', '100 2 1 ' + self.sha512_hash)