CDNSKEY = 60
OPENPGPKEY = 61
CSYNC = 62
+ ZONEMD = 63
SVCB = 64
HTTPS = 65
SPF = 99
CDNSKEY = RdataType.CDNSKEY
OPENPGPKEY = RdataType.OPENPGPKEY
CSYNC = RdataType.CSYNC
+ZONEMD = RdataType.ZONEMD
SVCB = RdataType.SVCB
HTTPS = RdataType.HTTPS
SPF = RdataType.SPF
--- /dev/null
+# 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)
'TXT',
'URI',
'X25',
+ 'ZONEMD',
]
"""DNS Zones."""
import contextlib
+import hashlib
import io
import os
+import struct
import dns.exception
import dns.name
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
"""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.
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):
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"
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
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
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
--- /dev/null
+# 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)