]> git.ipfire.org Git - thirdparty/dnspython.git/commitdiff
Initial changes to prepare for SIG(0) (#1202)
authorJakob Schlyter <jakob@kirei.se>
Thu, 25 Sep 2025 18:01:43 +0000 (20:01 +0200)
committerGitHub <noreply@github.com>
Thu, 25 Sep 2025 18:01:43 +0000 (11:01 -0700)
* # 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 [new file with mode: 0644]
dns/rdtypes/ANY/RRSIG.py
dns/rdtypes/ANY/SIG.py [new file with mode: 0644]
dns/rdtypes/rrsigbase.py [new file with mode: 0644]
tests/test_rdtypeanykey.py [new file with mode: 0644]

diff --git a/dns/rdtypes/ANY/KEY.py b/dns/rdtypes/ANY/KEY.py
new file mode 100644 (file)
index 0000000..9b0df1c
--- /dev/null
@@ -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)
index 5556cbaccc1df74d3898d2376b8bd5660a22043a..64d1f0d24948862c6d737537ed669cf637b842dc 100644 (file)
 # 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 (file)
index 0000000..e58b45a
--- /dev/null
@@ -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 (file)
index 0000000..045fdc7
--- /dev/null
@@ -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 (file)
index 0000000..5fb60da
--- /dev/null
@@ -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()