Commit 02327357ec for bind
commit 02327357ecf9ae4cb25fb74e4f4ed68d5bdc1165
Author: Ondřej Surý <ondrej@isc.org>
Date: Tue Nov 3 15:37:01 2026 +0100
Return FORMERR for standalone RRSIG queries
Signatures are returned with the RRsets they cover when DNSSEC is
requested. Reject standalone RRSIG queries with FORMERR before lookup
or recursion, and remove the special handling that is now unreachable.
Assisted-by: Claude:claude-fable-5-1
diff --git a/bin/tests/system/additional/tests.sh b/bin/tests/system/additional/tests.sh
index aa199ebd7a..3a69d78451 100644
--- a/bin/tests/system/additional/tests.sh
+++ b/bin/tests/system/additional/tests.sh
@@ -319,6 +319,18 @@ if [ $ret -eq 1 ]; then
status=$((status + 1))
fi
+n=$((n + 1))
+echo_i "testing FORMERR response to RRSIG query ($n)"
+ret=0
+$DIG $DIGOPTS -t RRSIG www.rt.example @10.53.0.1 >dig.out.$n || ret=1
+grep "status: FORMERR" dig.out.$n >/dev/null || ret=1
+grep "ANSWER: 0" dig.out.$n >/dev/null || ret=1
+grep "EDE: 30" dig.out.$n >/dev/null || ret=1
+if [ $ret -eq 1 ]; then
+ echo_i "failed"
+ status=$((status + 1))
+fi
+
echo_i "testing with 'minimal-responses no-auth;'"
minimal=no-auth
dotests
diff --git a/bin/tests/system/digdelv/tests_digdelv_delv.py b/bin/tests/system/digdelv/tests_digdelv_delv.py
index bd0b01f1f4..49d10e1419 100644
--- a/bin/tests/system/digdelv/tests_digdelv_delv.py
+++ b/bin/tests/system/digdelv/tests_digdelv_delv.py
@@ -254,6 +254,15 @@ def test_any_query(delv, ns3):
assert check_ttl_range(result.out, "SOA", 300)
+def test_rrsig_query(delv, ns3):
+ """
+ Check that delv -t RRSIG fails; RRSIG queries are never sent.
+ """
+ result = delv(f"@{ns3.ip} -t RRSIG example")
+ assert ";; resolution failed: FORMERR" in result.err
+ assert Re(r"^example\.") not in result.out
+
+
@pytest.mark.parametrize(
"anchor",
[
diff --git a/bin/tests/system/dnssec/tests_validation.py b/bin/tests/system/dnssec/tests_validation.py
index af97e44c92..6d9d795c81 100644
--- a/bin/tests/system/dnssec/tests_validation.py
+++ b/bin/tests/system/dnssec/tests_validation.py
@@ -14,7 +14,7 @@ from re import compile as Re
import shutil
import time
-from dns import flags, name, rdataclass, rdatatype
+from dns import flags, name, rcode, rdataclass, rdatatype
from dns.edns import EDECode
import pytest
@@ -73,18 +73,23 @@ def test_load_transfer(qname, qtype):
isctest.check.noerror(res1)
-def test_insecure_rrsig():
- # check that for a rrsig query against a validating resolver where the
- # authoritative zone is unsigned (insecure delegation), noerror is
- # returned.
- msg = isctest.query.create("a.insecure.example", "RRSIG")
- res = isctest.query.tcp(msg, "10.53.0.4")
- isctest.check.noerror(res)
- isctest.check.rr_count_eq(res.answer, 0)
- isctest.check.rr_count_eq(res.authority, 1)
- isctest.check.rr_count_eq(res.additional, 0)
- assert str(res.authority[0].name) == "insecure.example."
- assert res.authority[0].rdtype == rdatatype.SOA
+@pytest.mark.parametrize("transport", ["udp", "tcp"])
+@pytest.mark.parametrize(
+ "use_edns,dnssec", [(False, False), (True, False), (True, True)]
+)
+def test_rrsig_query_formerr(transport, use_edns, dnssec):
+ # Standalone RRSIG queries fail for signed and unsigned names,
+ # on authoritative servers and resolvers, with and without EDNS.
+ for server in ("10.53.0.3", "10.53.0.4"):
+ for qname in ("secure.example", "a.insecure.example"):
+ msg = isctest.query.create(qname, "RRSIG", use_edns=use_edns, dnssec=dnssec)
+ res = getattr(isctest.query, transport)(
+ msg, server, expected_rcode=rcode.FORMERR
+ )
+ isctest.check.formerr(res)
+ isctest.check.empty_answer(res)
+ if use_edns:
+ isctest.check.ede(res, 30)
def test_adflag():
@@ -706,27 +711,24 @@ def test_cache(ns4):
for rrset in res2.answer:
assert rrset.ttl <= 300
- # first query for a NS record, to cache NSEC and RRSIG(NSEC)
- msg = isctest.query.create("normalthenrrsig.secure.example", "NS")
- isctest.query.tcp(msg, "10.53.0.4")
- # query for a record, then follow it with a query for the
- # corresponding RRSIG, check that it's answered from the cache
+ # RRSIG queries return FORMERR, no matter what the cache
+ # holds for the queried name: prime the cache with a normal query,
+ # then follow it with a query for the corresponding RRSIG
msg = isctest.query.create("normalthenrrsig.secure.example", "A")
isctest.query.tcp(msg, "10.53.0.4")
msg = isctest.query.create("normalthenrrsig.secure.example", "RRSIG")
res1 = isctest.query.tcp(msg, "10.53.0.3")
res2 = isctest.query.tcp(msg, "10.53.0.4")
- isctest.check.same_answer(res1, res2)
- isctest.check.noerror(res2)
- isctest.check.raflag(res2)
+ for res in (res1, res2):
+ isctest.check.formerr(res)
+ isctest.check.empty_answer(res)
+ isctest.check.ede(res, 30)
- # check direct query for RRSIG: if it's not cached with other records,
- # it should result in an empty response.
msg = isctest.query.create("rrsigonly.secure.example", "RRSIG")
res1 = isctest.query.tcp(msg, "10.53.0.4")
+ isctest.check.formerr(res1)
isctest.check.empty_answer(res1)
- isctest.check.noraflag(res1)
# check that a DNSKEY query with no data still gets cached
msg = isctest.query.create("insecure.example", "DNSKEY")
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 ac42a7cb1c..ab10c4cc81 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
@@ -37,11 +37,16 @@ REJECTED_TYPES = META_TYPES + SIG_TYPES
@pytest.mark.parametrize("covered_type", REJECTED_TYPES)
-def test_rrsig_covers_rejected_type_is_servfail(covered_type):
+def test_rrsig_query_returns_formerr(covered_type):
+ """
+ The resolver rejects RRSIG queries with FORMERR, so the malformed
+ RRSIG can never be pulled into the cache with a direct RRSIG
+ query.
+ """
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)
+ isctest.check.formerr(res)
@pytest.mark.parametrize("covered_type", REJECTED_TYPES)
diff --git a/bin/tests/system/resolver/tests.sh b/bin/tests/system/resolver/tests.sh
index 5e32d59a88..bbe9747990 100755
--- a/bin/tests/system/resolver/tests.sh
+++ b/bin/tests/system/resolver/tests.sh
@@ -326,7 +326,7 @@ if [ $ret = 2 ]; then echo_i "ncache priming failed"; fi
dig_with_opts +tcp mx example.net @10.53.0.7 >dig.ns7.out.${n} || ret=3
grep "status: NOERROR" dig.ns7.out.${n} >/dev/null || ret=3
dig_with_opts +tcp rrsig mail.example.net +norec @10.53.0.7 >dig.ns7.out.${n} || ret=4
-grep "status: SERVFAIL" dig.ns7.out.${n} >/dev/null || ret=4
+grep "status: FORMERR" dig.ns7.out.${n} >/dev/null || ret=4
grep "ANSWER: 0" dig.ns7.out.${n} >/dev/null || ret=4
dig_with_opts +tcp aaaa mail.example.net +norec @10.53.0.7 >dig.ns7.out.${n} || ret=5
grep "status: SERVFAIL" dig.ns7.out.${n} >/dev/null || ret=5
diff --git a/bin/tests/system/rpz/tests.sh b/bin/tests/system/rpz/tests.sh
index 12bdba62ea..dbfc5f7428 100644
--- a/bin/tests/system/rpz/tests.sh
+++ b/bin/tests/system/rpz/tests.sh
@@ -650,7 +650,8 @@ done
# resolving foo.
# nxdomain 32.3.2.1.127.rpz-ip
end_group
-ckstats $ns3 bugs ns3 8
+# RRSIG queries are rejected before RPZ processing.
+ckstats $ns3 bugs ns3 6
# Ensure ns3 manages to transfer the fast-expire zone before shutdown.
nextpartreset ns3/named.run
diff --git a/doc/arm/general.rst b/doc/arm/general.rst
index 3127d06774..05b359b02a 100644
--- a/doc/arm/general.rst
+++ b/doc/arm/general.rst
@@ -477,3 +477,17 @@ archival, and they should not be quoted or cited in any formal documents
unless accompanied by the disclaimer that they are "works in progress."
IDs have a lifespan of six months, after which they are deleted unless
updated by their authors.
+
+BIND 9 implements the following Internet Drafts:
+
+`draft-sury-dnsop-rrsig-refused`_ - O. Surý, J. Včelák, and P. Špaček.
+*Refusing DNS Queries That Have QTYPE=RRSIG.* Work in progress.
+
+Queries of type RRSIG are answered with FORMERR by both authoritative
+and recursive servers; when the query uses EDNS, the response includes
+Extended DNS Error code 30 (Invalid Query Type). The resolver and
+:iscman:`delv` never send such queries. To obtain signatures, query for
+the covered record type with DNSSEC enabled; its covering RRSIG records
+are included in the response.
+
+.. _draft-sury-dnsop-rrsig-refused: https://datatracker.ietf.org/doc/draft-sury-dnsop-rrsig-refused/
diff --git a/doc/arm/reference.rst b/doc/arm/reference.rst
index f0d19b4db5..2d65fac94c 100644
--- a/doc/arm/reference.rst
+++ b/doc/arm/reference.rst
@@ -2077,9 +2077,8 @@ Boolean Options
If set to ``yes``, the server replies with only one of
the RRsets for the query name, and its covering RRSIGs if any,
when generating a positive response to a query of type ANY,
- instead of replying with all known RRsets for the name. Similarly, a
- query for type RRSIG is answered with the RRSIG records covering
- only one type. This can reduce the impact of some kinds of attack
+ instead of replying with all known RRsets for the name.
+ This can reduce the impact of some kinds of attack
traffic, without harming legitimate clients. (Note, however, that the
RRset returned is the first one found in the database; it is not
necessarily the smallest available RRset.) Additionally,
diff --git a/lib/dns/client.c b/lib/dns/client.c
index 672344fce9..60d71bbdc4 100644
--- a/lib/dns/client.c
+++ b/lib/dns/client.c
@@ -452,15 +452,8 @@ static isc_result_t
view_find(resctx_t *rctx, dns_db_t **dbp, dns_name_t *foundname) {
isc_result_t result;
dns_name_t *name = dns_name(&rctx->name);
- dns_rdatatype_t type;
-
- if (rctx->type == dns_rdatatype_rrsig) {
- type = dns_rdatatype_any;
- } else {
- type = rctx->type;
- }
- result = dns_view_find(rctx->view, name, type, 0, 0, false, dbp,
+ result = dns_view_find(rctx->view, name, rctx->type, 0, 0, false, dbp,
foundname, rctx->rdataset, rctx->sigrdataset);
return result;
@@ -869,6 +862,14 @@ startresolve(dns_client_t *client, const dns_name_t *name,
REQUIRE(transp != NULL && *transp == NULL);
REQUIRE(rdclass == dns_rdataclass_in);
+ /*
+ * RRSIG queries are not supported by the resolver. Reject them
+ * before looking in the local database as well.
+ */
+ if (dns_rdatatype_issig(type)) {
+ return DNS_R_FORMERR;
+ }
+
mctx = client->mctx;
want_dnssec = ((options & DNS_CLIENTRESOPT_NODNSSEC) == 0);
want_validation = ((options & DNS_CLIENTRESOPT_NOVALIDATE) == 0);
diff --git a/lib/dns/include/dns/resolver.h b/lib/dns/include/dns/resolver.h
index 630d926f0e..78cfe1b6cb 100644
--- a/lib/dns/include/dns/resolver.h
+++ b/lib/dns/include/dns/resolver.h
@@ -303,6 +303,9 @@ dns_resolver_createfetch(dns_resolver_t *res, const dns_name_t *name,
* must remain stable until after 'action' has been called or
* dns_resolver_cancelfetch() is called.
*
+ *\li Fetches for type RRSIG are rejected with #DNS_R_FORMERR;
+ * no such queries are sent to authoritative servers.
+ *
* Requires:
*
*\li 'res' is a valid resolver that has been frozen.
@@ -333,6 +336,7 @@ dns_resolver_createfetch(dns_resolver_t *res, const dns_name_t *name,
*\li #ISC_R_SUCCESS Success
*\li #DNS_R_DUPLICATE
*\li #DNS_R_DROP
+ *\li #DNS_R_FORMERR 'type' is RRSIG
*
*\li Many other values are possible, all of which indicate failure.
*/
diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c
index 5f91c23153..c55e50909a 100644
--- a/lib/dns/resolver.c
+++ b/lib/dns/resolver.c
@@ -4609,8 +4609,7 @@ resume_qmin(void *arg) {
if ((result == DNS_R_CNAME || result == DNS_R_DNAME) &&
fctx->qmin_labels == dns_name_countlabels(fctx->name) &&
fctx->type != dns_rdatatype_nsec &&
- fctx->type != dns_rdatatype_any &&
- fctx->type != dns_rdatatype_rrsig)
+ fctx->type != dns_rdatatype_any)
{
pull_from_resp(resp, fctx);
@@ -4940,6 +4939,7 @@ fctx__create(dns_resolver_t *res, isc_loop_t *loop, const dns_name_t *name,
* Caller must be holding the lock for 'bucket'
*/
REQUIRE(fctxp != NULL && *fctxp == NULL);
+ REQUIRE(!dns_rdatatype_issig(type));
fctx = isc_mem_get(mctx, sizeof(*fctx));
*fctx = (fetchctx_t){ .type = type,
@@ -8200,13 +8200,9 @@ rctx_answer_init(respctx_t *rctx) {
}
/*
- * There can be multiple RRSIG records at a name so
- * we treat these types as a subset of ANY.
+ * RRSIG fetches are never created, see fctx__create().
*/
rctx->type = fctx->type;
- if (dns_rdatatype_issig(fctx->type)) {
- rctx->type = dns_rdatatype_any;
- }
/*
* Bigger than any valid DNAME label count.
@@ -8828,12 +8824,6 @@ rctx_answer_any(respctx_t *rctx) {
return ISC_R_COMPLETE;
}
- if (dns_rdatatype_issig(fctx->type) &&
- rdataset->type != fctx->type)
- {
- continue;
- }
-
if (dns_rdatatype_isaddr(rdataset->type) &&
!is_answeraddress_allowed(fctx->res->view, rctx->aname,
rdataset))
@@ -8861,19 +8851,6 @@ rctx_answer_any(respctx_t *rctx) {
rdataset->trust = rctx->trust;
}
- /*
- * An RRSIG query is handled as a subset of ANY; if every record in
- * the answer was filtered out above, nothing was marked cacheable,
- * so there is nothing to cache, validate, or chase. Treat that as a
- * broken answer instead of returning success with no answer, which
- * would leave the fetch waiting for a validator that is never
- * started.
- */
- if (!dns_linkedname_attrs(rctx->aname)->cache) {
- rctx->result = DNS_R_FORMERR;
- return ISC_R_COMPLETE;
- }
-
return ISC_R_SUCCESS;
}
@@ -10712,6 +10689,11 @@ dns_resolver_createfetch(dns_resolver_t *res, const dns_name_t *name,
REQUIRE(VALID_RESOLVER(res));
REQUIRE(res->frozen);
/* XXXRTH Check for meta type */
+
+ if (dns_rdatatype_issig(type)) {
+ return DNS_R_FORMERR;
+ }
+
if (domain != NULL) {
REQUIRE(DNS_DELEGSET_VALID(delegset));
} else {
diff --git a/lib/ns/query.c b/lib/ns/query.c
index 9d48e43478..c16d2bfe9f 100644
--- a/lib/ns/query.c
+++ b/lib/ns/query.c
@@ -3067,6 +3067,12 @@ rpz_find_p(ns_client_t *client, dns_name_t *self_name, dns_rdatatype_t qtype,
dns_clientinfo_t ci;
bool found_a = false;
+ /*
+ * RRSIG queries are rejected before any RPZ processing, see
+ * ns_query_start().
+ */
+ REQUIRE(!dns_rdatatype_issig(qtype));
+
CTRACE(ISC_LOG_DEBUG(3), "rpz_find_p");
REQUIRE(foundname != NULL);
@@ -3151,14 +3157,9 @@ rpz_find_p(ns_client_t *client, dns_name_t *self_name, dns_rdatatype_t qtype,
*/
dns_rdataset_cleanup(*rdatasetp);
- if (dns_rdatatype_issig(qtype)) {
- result = DNS_R_NXRRSET;
- } else {
- result = dns_db_findext(
- *dbp, p_name, *versionp, qtype, 0,
- client->inner.now, found, &cm, &ci,
- *rdatasetp, NULL);
- }
+ result = dns_db_findext(*dbp, p_name, *versionp, qtype,
+ 0, client->inner.now, found,
+ &cm, &ci, *rdatasetp, NULL);
}
}
switch (result) {
@@ -4752,13 +4753,6 @@ qctx_init(ns_client_t *client, dns_fetchresponse_t **frespp,
qctx->findcoveringnsec = qctx->view->synthfromdnssec;
dns_fixedname_init(&qctx->foundname);
dns_fixedname_init(&qctx->zfoundname);
-
- /*
- * If it's an RRSIG or SIG query, we'll iterate the node.
- */
- if (dns_rdatatype_issig(qctx->qtype)) {
- qctx->type = dns_rdatatype_any;
- }
}
/*
@@ -6187,11 +6181,7 @@ query_resume(query_ctx_t *qctx) {
}
INSIST(qctx->rdataset != NULL);
- if (dns_rdatatype_issig(qctx->qtype)) {
- qctx->type = dns_rdatatype_any;
- } else {
- qctx->type = qctx->qtype;
- }
+ qctx->type = qctx->qtype;
CALL_HOOK(NS_QUERY_RESUME_RESTORED, qctx);
@@ -11433,6 +11423,18 @@ ns_query_start(ns_client_t *client, isc_nmhandle_t *handle) {
}
}
+ /*
+ * Standalone RRSIG queries are not supported. Signatures are
+ * returned with the RRsets they cover when DNSSEC is requested.
+ * Return FORMERR on authoritative and recursive servers alike.
+ */
+ if (dns_rdatatype_issig(qtype)) {
+ dns_ede_add(&client->edectx, DNS_EDE_INVALIDQTYPE,
+ "RRSIG queries are not supported");
+ query_error(client, DNS_R_FORMERR, __LINE__);
+ return;
+ }
+
/*
* Turn on minimal response for (C)DNSKEY and (C)DS queries.
*/
@@ -11473,9 +11475,7 @@ ns_query_start(ns_client_t *client, isc_nmhandle_t *handle) {
* We don't need to set DNS_DBFIND_PENDINGOK when validation is
* disabled as there will be no pending data.
*/
- if ((message->flags & DNS_MESSAGEFLAG_CD) != 0 ||
- qtype == dns_rdatatype_rrsig)
- {
+ if ((message->flags & DNS_MESSAGEFLAG_CD) != 0) {
client->query.dboptions |= DNS_DBFIND_PENDINGOK;
client->query.fetchoptions |= DNS_FETCHOPT_NOVALIDATE;
} else if (!client->inner.view->enablevalidation) {
diff --git a/tests/dns/resolver_test.c b/tests/dns/resolver_test.c
index ef54b53a2c..fd3c0e706e 100644
--- a/tests/dns/resolver_test.c
+++ b/tests/dns/resolver_test.c
@@ -32,7 +32,9 @@
#include <dns/dispatch.h>
#include <dns/lib.h>
#include <dns/name.h>
+#include <dns/rdataset.h>
#include <dns/resolver.h>
+#include <dns/result.h>
#include <dns/view.h>
#include <tests/dns.h>
@@ -101,6 +103,34 @@ ISC_LOOP_TEST_IMPL(create) {
isc_loopmgr_shutdown();
}
+static void
+unexpected_fetch_done(void *arg) {
+ UNUSED(arg);
+ fail_msg("an RRSIG query must not create a fetch");
+}
+
+ISC_LOOP_TEST_IMPL(rrsig_formerr) {
+ dns_resolver_t *resolver = NULL;
+ dns_fetch_t *fetch = NULL;
+ dns_rdataset_t rdataset;
+ isc_result_t result;
+
+ mkres(&resolver);
+ dns_resolver_freeze(resolver);
+ dns_rdataset_init(&rdataset);
+
+ result = dns_resolver_createfetch(
+ resolver, dns_rootname, dns_rdatatype_rrsig, NULL, NULL, NULL,
+ NULL, 0, 0, 0, NULL, NULL, NULL, isc_loop(),
+ unexpected_fetch_done, NULL, NULL, &rdataset, NULL, &fetch);
+ assert_int_equal(result, DNS_R_FORMERR);
+ assert_null(fetch);
+ assert_false(dns_rdataset_isassociated(&rdataset));
+
+ destroy_resolver(&resolver);
+ isc_loopmgr_shutdown();
+}
+
/* dns_resolver_gettimeout */
ISC_LOOP_TEST_IMPL(gettimeout) {
dns_resolver_t *resolver = NULL;
@@ -188,6 +218,7 @@ ISC_LOOP_TEST_IMPL(settimeout_overmax) {
ISC_TEST_LIST_START
ISC_TEST_ENTRY_CUSTOM(create, setup_test, teardown_test)
+ISC_TEST_ENTRY_CUSTOM(rrsig_formerr, setup_test, teardown_test)
ISC_TEST_ENTRY_CUSTOM(gettimeout, setup_test, teardown_test)
ISC_TEST_ENTRY_CUSTOM(settimeout, setup_test, teardown_test)
ISC_TEST_ENTRY_CUSTOM(settimeout_default, setup_test, teardown_test)