]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Add a regression test for an RRSIG that covers a signature
authorOndřej Surý <ondrej@isc.org>
Thu, 2 Jul 2026 08:21:40 +0000 (10:21 +0200)
committerOndřej Surý <ondrej@isc.org>
Thu, 2 Jul 2026 11:26:48 +0000 (13:26 +0200)
The qpcache_rrsig_any test enumerated only meta-types as the covered
type, so an RRSIG covering RRSIG -- a non-meta signature type -- slipped
through the earlier meta-type hardening. Probe that case too.

Assisted-by: Claude:claude-opus-4-8
bin/tests/system/qpcache_rrsig_any/ans3/ans.py
bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py

index 67993fc94e9ee6748ed25e243d8c6d6e9d38a1ce..e197afc7325eb6e302bbb60915ddb3e07d90483d 100644 (file)
@@ -10,11 +10,17 @@ file, you can obtain one at https://mozilla.org/MPL/2.0/.
 See the COPYRIGHT file distributed with this work for additional
 information regarding copyright ownership.
 
-For any query, returns a hand-crafted RRSIG whose Type-Covered field
-is selected by the leftmost label of QNAME. The label is parsed as a
-DNS type via `dns.rdatatype.from_text()`, so the resolver can be
+For any query, returns hand-crafted RRSIG records whose Type-Covered
+field is selected by the leftmost label of QNAME. The label is parsed
+as a DNS type via `dns.rdatatype.from_text()`, so the resolver can be
 probed with any meta-type by querying e.g. `any.attacker.test.`,
 `axfr.attacker.test.`, `tsig.attacker.test.`, etc.
+
+Tripping the resolver's QP-cache RRSIG-pairing assertion needs a second
+RRSIG header co-located at the owner name, so when the covered type is
+itself a signature (`rrsig.attacker.test.`) the answer also carries two
+ordinary RRSIGs (covering A and AAAA) next to the RRSIG-covers-RRSIG
+poison. A single RRSIG-covers-RRSIG record is cached harmlessly.
 """
 
 from collections.abc import AsyncGenerator
@@ -39,17 +45,23 @@ class RrsigCoversHandler(ResponseHandler):
     ) -> AsyncGenerator[DnsResponseSend, None]:
         covers_label = qctx.qname.labels[0].decode("ascii").upper()
         covers = dns.rdatatype.from_text(covers_label)
-        rrset = dns.rrset.from_text(
-            qctx.qname,
-            3600,
-            dns.rdataclass.IN,
-            dns.rdatatype.RRSIG,
-            f"TYPE{int(covers)} 8 2 3600 20300101000000 20200101000000 "
-            "12345 attacker.test. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
-        )
+
+        def rrsig_covering(covered: dns.rdatatype.RdataType) -> dns.rrset.RRset:
+            return dns.rrset.from_text(
+                qctx.qname,
+                3600,
+                dns.rdataclass.IN,
+                dns.rdatatype.RRSIG,
+                f"TYPE{int(covered)} 8 2 3600 20300101000000 20200101000000 "
+                "12345 attacker.test. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+            )
+
         qctx.response.set_rcode(dns.rcode.NOERROR)
         qctx.response.flags |= dns.flags.AA
-        qctx.response.answer.append(rrset)
+        if covers == dns.rdatatype.RRSIG:
+            qctx.response.answer.append(rrsig_covering(dns.rdatatype.A))
+            qctx.response.answer.append(rrsig_covering(dns.rdatatype.AAAA))
+        qctx.response.answer.append(rrsig_covering(covers))
         yield DnsResponseSend(qctx.response)
 
 
index 8601eb43466a9eca3235deb57bd5ede930ace60b..c9436f1bd4cad2673984f1214cdb13750ab48b78 100644 (file)
 # information regarding copyright ownership.
 
 """
-A signature cannot cover a DNS meta-type. An RRSIG whose Type-Covered
-field is one of NONE/ANY/AXFR/IXFR/MAILA/MAILB/OPT/TSIG/TKEY is
-malformed and must be rejected by the resolver. ns3 picks the
-Type-Covered field from the leftmost label of QNAME.
+A signature must cover a real, non-signature rdata type. An RRSIG whose
+Type-Covered field is a meta-type (NONE/ANY/AXFR/IXFR/MAILA/MAILB/OPT/
+TSIG/TKEY) or a signature type (RRSIG) is malformed and must be rejected
+by the resolver. ns3 picks the Type-Covered field from the leftmost
+label of QNAME.
 """
 
 import pytest
@@ -31,25 +32,30 @@ pytestmark = pytest.mark.extra_artifacts(
 
 
 META_TYPES = ["ANY", "AXFR", "IXFR", "MAILA", "MAILB", "OPT", "TSIG", "TKEY"]
+# A signature covering a signature (RRSIG covering RRSIG) is not a meta-type,
+# so it slipped past the meta-type hardening and tripped an assertion in the
+# QP cache; make sure it is rejected too.
+SIG_TYPES = ["RRSIG"]
+REJECTED_TYPES = META_TYPES + SIG_TYPES
 
 
-@pytest.mark.parametrize("meta_type", META_TYPES)
-def test_rrsig_covers_metatype_is_servfail(meta_type):
-    qname = f"{meta_type.lower()}.attacker.test."
+@pytest.mark.parametrize("covered_type", REJECTED_TYPES)
+def test_rrsig_covers_rejected_type_is_servfail(covered_type):
+    qname = f"{covered_type.lower()}.attacker.test."
     msg = isctest.query.create(qname, "RRSIG", dnssec=False, ad=False)
     res = isctest.query.tcp(msg, "10.53.0.2")
     isctest.check.servfail(res)
 
 
-@pytest.mark.parametrize("meta_type", META_TYPES)
-def test_dig_nobesteffort_rejects_malformed_rrsig(meta_type, named_port):
+@pytest.mark.parametrize("covered_type", REJECTED_TYPES)
+def test_dig_nobesteffort_rejects_malformed_rrsig(covered_type, named_port):
     """
     With +nobesteffort, dig uses the same strict parser path that the
     recursive resolver uses, so a malformed RRSIG covering a meta-type
-    is rejected before being printed.
+    or a signature type is rejected before being printed.
     """
     dig = isctest.run.EnvCmd("DIG", f"-p {named_port}")
-    qname = f"{meta_type.lower()}.attacker.test."
+    qname = f"{covered_type.lower()}.attacker.test."
     res = dig(
         f"+nobesteffort +tries=1 +time=5 @10.53.0.3 {qname} RRSIG",
         raise_on_exception=False,
@@ -58,15 +64,15 @@ def test_dig_nobesteffort_rejects_malformed_rrsig(meta_type, named_port):
     assert "ANSWER SECTION" not in res.out
 
 
-@pytest.mark.parametrize("meta_type", META_TYPES)
-def test_dig_besteffort_shows_malformed_rrsig(meta_type, named_port):
+@pytest.mark.parametrize("covered_type", REJECTED_TYPES)
+def test_dig_besteffort_shows_malformed_rrsig(covered_type, named_port):
     """
     The default dig parser runs in +besteffort mode, which intentionally
     keeps wire-level inspection working: the malformed RRSIG is still
     printed so operators can debug what an upstream actually sent.
     """
     dig = isctest.run.EnvCmd("DIG", f"-p {named_port}")
-    qname = f"{meta_type.lower()}.attacker.test."
+    qname = f"{covered_type.lower()}.attacker.test."
     res = dig(f"+tries=1 +time=5 @10.53.0.3 {qname} RRSIG")
     assert "ANSWER SECTION" in res.out
     assert "RRSIG" in res.out