From: Ondřej Surý Date: Thu, 2 Jul 2026 08:21:40 +0000 (+0200) Subject: Add a regression test for an RRSIG that covers a signature X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=6c968be47d1f5d2f1831aaab15f0f240d3791c9e;p=thirdparty%2Fbind9.git Add a regression test for an RRSIG that covers a signature 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 --- diff --git a/bin/tests/system/qpcache_rrsig_any/ans3/ans.py b/bin/tests/system/qpcache_rrsig_any/ans3/ans.py index 67993fc94e9..e197afc7325 100644 --- a/bin/tests/system/qpcache_rrsig_any/ans3/ans.py +++ b/bin/tests/system/qpcache_rrsig_any/ans3/ans.py @@ -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) diff --git a/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py b/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py index 8601eb43466..c9436f1bd4c 100644 --- a/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py +++ b/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py @@ -12,10 +12,11 @@ # 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