From 4c72db9788ce826c3dc156785583cf733f1ea9aa Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Thu, 25 Sep 2025 20:01:43 +0200 Subject: [PATCH] Initial changes to prepare for SIG(0) (#1202) * # This is a combination of 6 commits. # This is the 1st commit message: Initial changes to prepare for SIG(0): - Add shared RRSIGBase for code shared between RRSIG and SIG - Add KEY RR - Add SIG RR # This is the commit message #2: Parse flags mnemonics and symbolic protocol names # This is the commit message #3: RFC 2535 section 7.1 says "Note that if the type flags field has the NOKEY value, nothing appears after the algorithm octet." # This is the commit message #4: Include sphinx only for Python 3.11 or later (#1225) * Include sphinx only for Python 3.11 or later * Use python_version # This is the commit message #5: Save token before returning it (for exception handling) # This is the commit message #6: Replace get/unget with plain unget and last token * Initial changes to prepare for SIG(0): - Add shared RRSIGBase for code shared between RRSIG and SIG - Add KEY RR - Add SIG RR - Parse flags mnemonics and symbolic protocol names * Make pyright happy --- dns/rdtypes/ANY/KEY.py | 117 +++++++++++++++++++++++++++ dns/rdtypes/ANY/RRSIG.py | 140 +++----------------------------- dns/rdtypes/ANY/SIG.py | 33 ++++++++ dns/rdtypes/rrsigbase.py | 159 +++++++++++++++++++++++++++++++++++++ tests/test_rdtypeanykey.py | 44 ++++++++++ 5 files changed, 362 insertions(+), 131 deletions(-) create mode 100644 dns/rdtypes/ANY/KEY.py create mode 100644 dns/rdtypes/ANY/SIG.py create mode 100644 dns/rdtypes/rrsigbase.py create mode 100644 tests/test_rdtypeanykey.py diff --git a/dns/rdtypes/ANY/KEY.py b/dns/rdtypes/ANY/KEY.py new file mode 100644 index 00000000..9b0df1c7 --- /dev/null +++ b/dns/rdtypes/ANY/KEY.py @@ -0,0 +1,117 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import base64 + +import dns.enum +import dns.exception +import dns.immutable +import dns.rdtypes.dnskeybase # lgtm[py/import-and-import-from] + + + +class Protocol(dns.enum.IntEnum): + NONE = 0 + TLS = 1 + EMAIL = 2 + DNSSEC = 3 + IPSEC = 4 + ALL = 255 + + @classmethod + def _maximum(cls): + return 255 + + +class LegacyFlag(dns.enum.IntEnum): + NOCONF = 0x4000 + NOAUTH = 0x8000 + NOKEY = 0xC000 + FLAG2 = 0x2000 + EXTEND = 0x1000 + FLAG4 = 0x0800 + FLAG5 = 0x0400 + USER = 0x0000 + ZONE = 0x0100 + HOST = 0x0200 + NTYP3 = 0x0300 + FLAG8 = 0x0080 + FLAG9 = 0x0040 + FLAG10 = 0x0020 + FLAG11 = 0x0010 + SIG0 = 0x0000 + SIG1 = 0x0001 + SIG2 = 0x0002 + SIG3 = 0x0003 + SIG4 = 0x0004 + SIG5 = 0x0005 + SIG6 = 0x0006 + SIG7 = 0x0007 + SIG8 = 0x0008 + SIG9 = 0x0009 + SIG10 = 0x000A + SIG11 = 0x000B + SIG12 = 0x000C + SIG13 = 0x000D + SIG14 = 0x000E + SIG15 = 0x000F + + +DNS_KEYFLAG_TYPEMASK = LegacyFlag.NOAUTH | LegacyFlag.NOCONF + + +@dns.immutable.immutable +class KEY(dns.rdtypes.dnskeybase.DNSKEYBase): + """KEY record""" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + token = tok.get() + try: + flags = tok.as_uint16(token) + except dns.exception.SyntaxError: + flags_str = tok.as_string(token) + try: + flags = 0 + for mnemonic in flags_str.split("|"): + flags |= LegacyFlag[mnemonic].value + except KeyError: + raise dns.exception.SyntaxError(f"Invalid flags: {flags_str}") + + token = tok.get() + try: + protocol = tok.as_uint8(token) + except dns.exception.SyntaxError: + protocol_str = tok.as_string(token) + try: + protocol = Protocol[protocol_str].value + except KeyError: + raise dns.exception.SyntaxError(f"Invalid protocol: {protocol_str}") + + algorithm = tok.get_string() + + # RFC 2535 section 7.1 says "Note that if the type flags field has the + # NOKEY value, nothing appears after the algorithm octet." + if (flags & DNS_KEYFLAG_TYPEMASK) != LegacyFlag.NOKEY: + b64 = tok.concatenate_remaining_identifiers().encode() + key = base64.b64decode(b64) + else: + key = b"" + + return cls(rdclass, rdtype, flags, protocol, algorithm, key) diff --git a/dns/rdtypes/ANY/RRSIG.py b/dns/rdtypes/ANY/RRSIG.py index 5556cbac..64d1f0d2 100644 --- a/dns/rdtypes/ANY/RRSIG.py +++ b/dns/rdtypes/ANY/RRSIG.py @@ -15,141 +15,19 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -import base64 -import calendar -import struct -import time - -import dns.dnssectypes -import dns.exception import dns.immutable -import dns.rdata -import dns.rdatatype - - -class BadSigTime(dns.exception.DNSException): - """Time in DNS SIG or RRSIG resource record cannot be parsed.""" +import dns.rdtypes.rrsigbase # lgtm[py/import-and-import-from] +# pylint: disable=unused-import +from dns.rdtypes.rrsigbase import ( # noqa: F401 lgtm[py/unused-import] + BadSigTime, + posixtime_to_sigtime, + sigtime_to_posixtime, +) -def sigtime_to_posixtime(what): - if len(what) <= 10 and what.isdigit(): - return int(what) - if len(what) != 14: - raise BadSigTime - year = int(what[0:4]) - month = int(what[4:6]) - day = int(what[6:8]) - hour = int(what[8:10]) - minute = int(what[10:12]) - second = int(what[12:14]) - return calendar.timegm((year, month, day, hour, minute, second, 0, 0, 0)) - - -def posixtime_to_sigtime(what): - return time.strftime("%Y%m%d%H%M%S", time.gmtime(what)) +# pylint: enable=unused-import @dns.immutable.immutable -class RRSIG(dns.rdata.Rdata): +class RRSIG(dns.rdtypes.rrsigbase.RRSIGBase): """RRSIG record""" - - __slots__ = [ - "type_covered", - "algorithm", - "labels", - "original_ttl", - "expiration", - "inception", - "key_tag", - "signer", - "signature", - ] - - def __init__( - self, - rdclass, - rdtype, - type_covered, - algorithm, - labels, - original_ttl, - expiration, - inception, - key_tag, - signer, - signature, - ): - super().__init__(rdclass, rdtype) - self.type_covered = self._as_rdatatype(type_covered) - self.algorithm = dns.dnssectypes.Algorithm.make(algorithm) - self.labels = self._as_uint8(labels) - self.original_ttl = self._as_ttl(original_ttl) - self.expiration = self._as_uint32(expiration) - self.inception = self._as_uint32(inception) - self.key_tag = self._as_uint16(key_tag) - self.signer = self._as_name(signer) - self.signature = self._as_bytes(signature) - - def covers(self): - return self.type_covered - - def to_text(self, origin=None, relativize=True, **kw): - return ( - f"{dns.rdatatype.to_text(self.type_covered)} " - f"{self.algorithm} {self.labels} {self.original_ttl} " - f"{posixtime_to_sigtime(self.expiration)} " - f"{posixtime_to_sigtime(self.inception)} " - f"{self.key_tag} " - f"{self.signer.choose_relativity(origin, relativize)} " - f"{dns.rdata._base64ify(self.signature, **kw)}" # pyright: ignore - ) - - @classmethod - def from_text( - cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None - ): - type_covered = dns.rdatatype.from_text(tok.get_string()) - algorithm = dns.dnssectypes.Algorithm.from_text(tok.get_string()) - labels = tok.get_int() - original_ttl = tok.get_ttl() - expiration = sigtime_to_posixtime(tok.get_string()) - inception = sigtime_to_posixtime(tok.get_string()) - key_tag = tok.get_int() - signer = tok.get_name(origin, relativize, relativize_to) - b64 = tok.concatenate_remaining_identifiers().encode() - signature = base64.b64decode(b64) - return cls( - rdclass, - rdtype, - type_covered, - algorithm, - labels, - original_ttl, - expiration, - inception, - key_tag, - signer, - signature, - ) - - def _to_wire(self, file, compress=None, origin=None, canonicalize=False): - header = struct.pack( - "!HBBIIIH", - self.type_covered, - self.algorithm, - self.labels, - self.original_ttl, - self.expiration, - self.inception, - self.key_tag, - ) - file.write(header) - self.signer.to_wire(file, None, origin, canonicalize) - file.write(self.signature) - - @classmethod - def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): - header = parser.get_struct("!HBBIIIH") - signer = parser.get_name(origin) - signature = parser.get_remaining() - return cls(rdclass, rdtype, *header, signer, signature) # pyright: ignore diff --git a/dns/rdtypes/ANY/SIG.py b/dns/rdtypes/ANY/SIG.py new file mode 100644 index 00000000..e58b45ad --- /dev/null +++ b/dns/rdtypes/ANY/SIG.py @@ -0,0 +1,33 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.rrsigbase # lgtm[py/import-and-import-from] + +# pylint: disable=unused-import +from dns.rdtypes.rrsigbase import ( # noqa: F401 lgtm[py/unused-import] + BadSigTime, + posixtime_to_sigtime, + sigtime_to_posixtime, +) + +# pylint: enable=unused-import + + +@dns.immutable.immutable +class SIG(dns.rdtypes.rrsigbase.RRSIGBase): + """SIG record""" diff --git a/dns/rdtypes/rrsigbase.py b/dns/rdtypes/rrsigbase.py new file mode 100644 index 00000000..045fdc78 --- /dev/null +++ b/dns/rdtypes/rrsigbase.py @@ -0,0 +1,159 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import base64 +import calendar +import struct +import time + +import dns.dnssectypes +import dns.exception +import dns.immutable +import dns.rdata +import dns.rdatatype + + +class BadSigTime(dns.exception.DNSException): + + """Time in DNS SIG or RRSIG resource record cannot be parsed.""" + + +def sigtime_to_posixtime(what): + if len(what) <= 10 and what.isdigit(): + return int(what) + if len(what) != 14: + raise BadSigTime + year = int(what[0:4]) + month = int(what[4:6]) + day = int(what[6:8]) + hour = int(what[8:10]) + minute = int(what[10:12]) + second = int(what[12:14]) + return calendar.timegm((year, month, day, hour, minute, second, 0, 0, 0)) + + +def posixtime_to_sigtime(what): + return time.strftime("%Y%m%d%H%M%S", time.gmtime(what)) + + +@dns.immutable.immutable +class RRSIGBase(dns.rdata.Rdata): + + """Base class for rdata that is like a RRSIG record""" + + __slots__ = [ + "type_covered", + "algorithm", + "labels", + "original_ttl", + "expiration", + "inception", + "key_tag", + "signer", + "signature", + ] + + def __init__( + self, + rdclass, + rdtype, + type_covered, + algorithm, + labels, + original_ttl, + expiration, + inception, + key_tag, + signer, + signature, + ): + super().__init__(rdclass, rdtype) + self.type_covered = self._as_rdatatype(type_covered) + self.algorithm = dns.dnssectypes.Algorithm.make(algorithm) + self.labels = self._as_uint8(labels) + self.original_ttl = self._as_ttl(original_ttl) + self.expiration = self._as_uint32(expiration) + self.inception = self._as_uint32(inception) + self.key_tag = self._as_uint16(key_tag) + self.signer = self._as_name(signer) + self.signature = self._as_bytes(signature) + + def covers(self): + return self.type_covered + + def to_text(self, origin=None, relativize=True, **kw): + return "%s %d %d %d %s %s %d %s %s" % ( + dns.rdatatype.to_text(self.type_covered), + self.algorithm, + self.labels, + self.original_ttl, + posixtime_to_sigtime(self.expiration), + posixtime_to_sigtime(self.inception), + self.key_tag, + self.signer.choose_relativity(origin, relativize), + dns.rdata._base64ify(self.signature, **kw),# pyright: ignore + ) + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + type_covered = dns.rdatatype.from_text(tok.get_string()) + algorithm = dns.dnssectypes.Algorithm.from_text(tok.get_string()) + labels = tok.get_int() + original_ttl = tok.get_ttl() + expiration = sigtime_to_posixtime(tok.get_string()) + inception = sigtime_to_posixtime(tok.get_string()) + key_tag = tok.get_int() + signer = tok.get_name(origin, relativize, relativize_to) + b64 = tok.concatenate_remaining_identifiers().encode() + signature = base64.b64decode(b64) + return cls( + rdclass, + rdtype, + type_covered, + algorithm, + labels, + original_ttl, + expiration, + inception, + key_tag, + signer, + signature, + ) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + header = struct.pack( + "!HBBIIIH", + self.type_covered, + self.algorithm, + self.labels, + self.original_ttl, + self.expiration, + self.inception, + self.key_tag, + ) + file.write(header) + self.signer.to_wire(file, None, origin, canonicalize) + file.write(self.signature) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + header = parser.get_struct("!HBBIIIH") + signer = parser.get_name(origin) + signature = parser.get_remaining() + return cls(rdclass, rdtype, *header, signer, signature)# pyright: ignore diff --git a/tests/test_rdtypeanykey.py b/tests/test_rdtypeanykey.py new file mode 100644 index 00000000..5fb60daa --- /dev/null +++ b/tests/test_rdtypeanykey.py @@ -0,0 +1,44 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import unittest + +import dns.rrset + + +class RdtypeAnyKeyTestCase(unittest.TestCase): + def testFlagsRRToText(self): # type: () -> None + """Test that RR method returns correct flags.""" + + rr = dns.rrset.from_text("foo", 300, "IN", "KEY", "HOST 3 8 KEY=")[0] + self.assertEqual(rr.flags, 512) + + rr = dns.rrset.from_text("foo", 300, "IN", "KEY", "257 3 8 KEY=")[0] + self.assertEqual(rr.flags, 257) + + rr = dns.rrset.from_text("foo", 300, "IN", "KEY", "ZONE|SIG1 3 8 KEY=")[0] + self.assertEqual(rr.flags, 257) + + with self.assertRaises(dns.exception.SyntaxError): + _ = dns.rrset.from_text("foo", 300, "IN", "KEY", "ZONE|XYZZY 3 8 KEY=")[0] + + rr = dns.rrset.from_text("foo", 300, "IN", "KEY", "NOKEY 3 8")[0] + self.assertEqual(rr.flags, 49152) + self.assertEqual(rr.protocol, 3) + self.assertEqual(rr.algorithm, 8) + self.assertEqual(rr.key, b"") + + def testAlgorithmRRToText(self): # type: () -> None + """Test that RR method returns correct flags.""" + + rr = dns.rrset.from_text("foo", 300, "IN", "KEY", "257 DNSSEC 8 KEY=")[0] + self.assertEqual(rr.protocol, 3) + + rr = dns.rrset.from_text("foo", 300, "IN", "KEY", "257 IPSEC 8 KEY=")[0] + self.assertEqual(rr.protocol, 4) + + with self.assertRaises(dns.exception.SyntaxError): + _ = dns.rrset.from_text("foo", 300, "IN", "KEY", "257 XYZZY 8 KEY=")[0] + + +if __name__ == "__main__": + unittest.main() -- 2.47.3