fix: usr: Fix deferred validation of unsigned DS and DNSKEY records

When processing a query with the "checking disabled" bit set (CD=1), `named` stores the unvalidated result in the cache, marked "pending". When the same query is sent with CD=0, the cached data is validated, and either accepted as an answer, or ejected from the cache as invalid. This deferred validation was not attempted for DS and DNSKEY records if they had no cached signatures, causing spurious validation failures. We now complete the deferred validation in this scenario.

Also, if deferred validation fails, we now re-query the data to find out whether the zone has been corrected since the invalid data was cached.

Closes #5066

Merge branch '5066-fix-strip-dnssec-rrsigs' into 'main'

See merge request isc-projects/bind9!10104
This commit is contained in:
Mark Andrews 2025-02-16 23:36:25 +00:00
commit ebf1606f38
10 changed files with 243 additions and 62 deletions

View file

@ -39,3 +39,7 @@ too-many-iterations. NS ns2.too-many-iterations.
ns2.too-many-iterations. A 10.53.0.2
peer-ns-spoof NS ns2.peer-ns-spoof.
ns2.peer-ns-spoof. A 10.53.0.2
dnskey-rrsigs-stripped. NS ns2.dnskey-rrsigs-stripped.
ns2.dnskey-rrsigs-stripped. A 10.53.0.2
ds-rrsigs-stripped. NS ns2.ds-rrsigs-stripped.
ns2.ds-rrsigs-stripped. A 10.53.0.2

View file

@ -31,6 +31,8 @@ cp "../ns2/dsset-in-addr.arpa." .
cp "../ns2/dsset-too-many-iterations." .
cp "../ns2/dsset-lazy-ksk." .
cp "../ns2/dsset-peer-ns-spoof." .
cp "../ns2/dsset-dnskey-rrsigs-stripped." .
cp "../ns2/dsset-ds-rrsigs-stripped." .
grep "$DEFAULT_ALGORITHM_NUMBER [12] " "../ns2/dsset-algroll." >"dsset-algroll."
cp "../ns6/dsset-optout-tld." .

View file

@ -0,0 +1,27 @@
; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
;
; SPDX-License-Identifier: MPL-2.0
;
; This Source Code Form is subject to the terms of the Mozilla Public
; License, v. 2.0. If a copy of the MPL was not distributed with this
; 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.
$TTL 300 ; 5 minutes
@ IN SOA mname1. . (
2000042407 ; serial
20 ; refresh (20 seconds)
20 ; retry (20 seconds)
1814400 ; expire (3 weeks)
3600 ; minimum (1 hour)
)
NS ns2
NS ns3
ns2 A 10.53.0.2
ns3 A 10.53.0.3
a A 10.0.0.1
b A 10.0.0.2
d A 10.0.0.4

View file

@ -0,0 +1,27 @@
; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
;
; SPDX-License-Identifier: MPL-2.0
;
; This Source Code Form is subject to the terms of the Mozilla Public
; License, v. 2.0. If a copy of the MPL was not distributed with this
; 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.
$TTL 300 ; 5 minutes
@ IN SOA mname1. . (
2000042407 ; serial
20 ; refresh (20 seconds)
20 ; retry (20 seconds)
1814400 ; expire (3 weeks)
3600 ; minimum (1 hour)
)
NS ns2
NS ns3
ns2 A 10.53.0.2
ns3 A 10.53.0.3
a A 10.0.0.1
b A 10.0.0.2
d A 10.0.0.4

View file

@ -0,0 +1,29 @@
; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
;
; SPDX-License-Identifier: MPL-2.0
;
; This Source Code Form is subject to the terms of the Mozilla Public
; License, v. 2.0. If a copy of the MPL was not distributed with this
; 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.
$TTL 300 ; 5 minutes
@ IN SOA mname1. . (
2000042407 ; serial
20 ; refresh (20 seconds)
20 ; retry (20 seconds)
1814400 ; expire (3 weeks)
3600 ; minimum (1 hour)
)
NS ns2
NS ns3
ns2 A 10.53.0.2
ns3 A 10.53.0.3
child NS ns2.child
ns2.child A 10.53.0.2
a A 10.0.0.1
b A 10.0.0.2
d A 10.0.0.4

View file

@ -224,4 +224,19 @@ zone "peer.peer-ns-spoof" {
file "peer.peer-ns-spoof.db.signed";
};
zone "dnskey-rrsigs-stripped" {
type primary;
file "dnskey-rrsigs-stripped.db.signed";
};
zone "ds-rrsigs-stripped" {
type primary;
file "ds-rrsigs-stripped.db.signed";
};
zone "child.ds-rrsigs-stripped" {
type primary;
file "child.ds-rrsigs-stripped.db.signed";
};
include "trusted.conf";

View file

@ -387,3 +387,48 @@ ksk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -n zone -f KSK "$z
zsk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -n zone "$zone")
cat "$infile" "$ksk.key" "$zsk.key" >"$zonefile"
"$SIGNER" -g -o "$zone" "$zonefile" >/dev/null 2>&1
#
# A zone with the DNSKEY RRSIGS stripped
#
zone=dnskey-rrsigs-stripped
infile=dnskey-rrsigs-stripped.db.in
zonefile=dnskey-rrsigs-stripped.db
ksk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -n zone -f KSK "$zone")
zsk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -n zone "$zone")
cat "$infile" "$ksk.key" "$zsk.key" >"$zonefile"
"$SIGNER" -g -o "$zone" "$zonefile" >/dev/null 2>&1
"$CHECKZONE" -D -q -i local "$zone" "$zonefile.signed" \
| awk '$4 == "RRSIG" && $5 == "DNSKEY" { next } { print }' >"$zonefile.stripped"
"$CHECKZONE" -D -q -i local "$zone" "$zonefile.signed" \
| awk '$4 == "SOA" { $7 = $7 + 1; print; next } { print }' >"$zonefile.next"
"$SIGNER" -g -o "$zone" -f "$zonefile.next" "$zonefile.next" >/dev/null 2>&1
cp "$zonefile.stripped" "$zonefile.signed"
#
# A child zone for the stripped DS RRSIGs test
#
zone=child.ds-rrsigs-stripped
infile=child.ds-rrsigs-stripped.db.in
zonefile=child.ds-rrsigs-stripped.db
ksk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -n zone -f KSK "$zone")
zsk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -n zone "$zone")
cat "$infile" "$ksk.key" "$zsk.key" >"$zonefile"
"$SIGNER" -g -o "$zone" "$zonefile" >/dev/null 2>&1
#
# A zone with the DNSKEY RRSIGS stripped
#
zone=ds-rrsigs-stripped
infile=ds-rrsigs-stripped.db.in
zonefile=ds-rrsigs-stripped.db
ksk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -n zone -f KSK "$zone")
zsk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -n zone "$zone")
cat "$infile" "$ksk.key" "$zsk.key" >"$zonefile"
"$SIGNER" -g -o "$zone" "$zonefile" >/dev/null 2>&1
"$CHECKZONE" -D -q -i local "$zone" "$zonefile.signed" \
| awk '$4 == "RRSIG" && $5 == "DS" { next } { print }' >"$zonefile.stripped"
"$CHECKZONE" -D -q -i local "$zone" "$zonefile.signed" \
| awk '$4 == "SOA" { $7 = $7 + 1; print; next } { print }' >"$zonefile.next"
"$SIGNER" -g -o "$zone" -f "$zonefile.next" "$zonefile.next" >/dev/null 2>&1
cp "$zonefile.stripped" "$zonefile.signed"

View file

@ -204,6 +204,46 @@ n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "checking recovery from stripped DNSKEY RRSIG ($n)"
ret=0
# prime cache with DNSKEY without RRSIGs
dig_with_opts +noauth +cd dnskey-rrsigs-stripped. @10.53.0.4 dnskey >dig.out.prime.ns4.test$n || ret=1
grep ";; flags: qr rd ra cd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1" dig.out.prime.ns4.test$n >/dev/null || ret=1
grep "status: NOERROR" dig.out.prime.ns4.test$n >/dev/null || ret=1
grep "RRSIG.DNSKEY" dig.out.prime.ns4.test$n >/dev/null && ret=1
# reload server with properly signed zone
cp ns2/dnskey-rrsigs-stripped.db.next ns2/dnskey-rrsigs-stripped.db.signed
nextpart ns2/named.run >/dev/null
rndccmd 10.53.0.2 reload dnskey-rrsigs-stripped | sed 's/^/ns2 /' | cat_i
wait_for_log 5 "zone dnskey-rrsigs-stripped/IN: loaded serial 2000042408" ns2/named.run || ret=1
dig_with_opts +noauth b.dnskey-rrsigs-stripped. @10.53.0.2 a >dig.out.ns2.test$n || ret=1
dig_with_opts +noauth b.dnskey-rrsigs-stripped. @10.53.0.4 a >dig.out.ns4.test$n || ret=1
digcomp dig.out.ns2.test$n dig.out.ns4.test$n || ret=1
grep "flags:.*ad.*QUERY" dig.out.ns4.test$n >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "checking recovery from stripped DS RRSIG ($n)"
ret=0
# prime cache with DS without RRSIGs
dig_with_opts +noauth +cd child.ds-rrsigs-stripped. @10.53.0.4 ds >dig.out.prime.ns4.test$n || ret=1
grep ";; flags: qr rd ra cd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1" dig.out.prime.ns4.test$n >/dev/null || ret=1
grep "status: NOERROR" dig.out.prime.ns4.test$n >/dev/null || ret=1
grep "RRSIG.DS" dig.out.prime.ns4.test$n >/dev/null && ret=1
# reload server with properly signed zone
cp ns2/ds-rrsigs-stripped.db.next ns2/ds-rrsigs-stripped.db.signed
nextpart ns2/named.run >/dev/null
rndccmd 10.53.0.2 reload ds-rrsigs-stripped | sed 's/^/ns2 /' | cat_i
wait_for_log 5 "zone ds-rrsigs-stripped/IN: loaded serial 2000042408" ns2/named.run || ret=1
dig_with_opts +noauth b.child.ds-rrsigs-stripped. @10.53.0.2 a >dig.out.ns2.test$n || ret=1
dig_with_opts +noauth b.child.ds-rrsigs-stripped. @10.53.0.4 a >dig.out.ns4.test$n || ret=1
digcomp dig.out.ns2.test$n dig.out.ns4.test$n || ret=1
grep "flags:.*ad.*QUERY" dig.out.ns4.test$n >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "checking that 'example/DS' from the referral was used in previous validation ($n)"
ret=0
grep "query 'example/DS/IN' approved" ns1/named.run >/dev/null && ret=1

View file

@ -50,6 +50,13 @@ pytestmark = pytest.mark.extra_artifacts(
"ns2/cds-update.secure.id",
"ns2/cds-x.secure.db",
"ns2/cds.secure.db",
"ns2/dnskey-rrsigs-stripped.db",
"ns2/dnskey-rrsigs-stripped.db.next",
"ns2/dnskey-rrsigs-stripped.db.stripped",
"ns2/child.ds-rrsigs-stripped.db",
"ns2/ds-rrsigs-stripped.db",
"ns2/ds-rrsigs-stripped.db.next",
"ns2/ds-rrsigs-stripped.db.stripped",
"ns2/example.db",
"ns2/in-addr.arpa.db",
"ns2/lazy-ksk.db",

View file

@ -162,6 +162,10 @@ validator_logcreate(dns_validator_t *val, dns_name_t *name,
dns_rdatatype_t type, const char *caller,
const char *operation);
static isc_result_t
create_fetch(dns_validator_t *val, dns_name_t *name, dns_rdatatype_t type,
isc_job_cb callback, const char *caller);
/*%
* Ensure the validator's rdatasets are marked as expired.
*/
@ -611,13 +615,19 @@ validator_callback_dnskey(void *arg) {
result = validate_async_run(val, resume_answer);
}
} else {
if (result != DNS_R_BROKENCHAIN) {
expire_rdatasets(val);
}
validator_log(val, ISC_LOG_DEBUG(3),
"validator_callback_dnskey: got %s",
isc_result_totext(result));
result = DNS_R_BROKENCHAIN;
if (result != DNS_R_BROKENCHAIN) {
expire_rdatasets(val);
result = create_fetch(val, &val->siginfo->signer,
dns_rdatatype_dnskey,
fetch_callback_dnskey,
"validator_callback_dnskey");
if (result == ISC_R_SUCCESS) {
result = DNS_R_WAIT;
}
}
}
cleanup:
@ -636,8 +646,7 @@ static void
validator_callback_ds(void *arg) {
dns_validator_t *subvalidator = (dns_validator_t *)arg;
dns_validator_t *val = subvalidator->parent;
isc_result_t result;
isc_result_t eresult = subvalidator->result;
isc_result_t result = subvalidator->result;
val->subvalidator = NULL;
@ -647,7 +656,7 @@ validator_callback_ds(void *arg) {
}
validator_log(val, ISC_LOG_DEBUG(3), "in validator_callback_ds");
if (eresult == ISC_R_SUCCESS) {
if (result == ISC_R_SUCCESS) {
bool have_dsset;
dns_name_t *name;
validator_log(val, ISC_LOG_DEBUG(3), "%s with trust %s",
@ -669,13 +678,18 @@ validator_callback_ds(void *arg) {
result = validate_async_run(val, validate_dnskey);
}
} else {
if (eresult != DNS_R_BROKENCHAIN) {
expire_rdatasets(val);
}
validator_log(val, ISC_LOG_DEBUG(3),
"validator_callback_ds: got %s",
isc_result_totext(eresult));
result = DNS_R_BROKENCHAIN;
isc_result_totext(result));
if (result != DNS_R_BROKENCHAIN) {
expire_rdatasets(val);
result = create_fetch(val, val->name, dns_rdatatype_ds,
fetch_callback_ds,
"validator_callback_ds");
if (result == ISC_R_SUCCESS) {
result = DNS_R_WAIT;
}
}
}
cleanup:
@ -1126,14 +1140,13 @@ seek_dnskey(dns_validator_t *val) {
* We have an rrset for the given keyname.
*/
val->keyset = &val->frdataset;
if ((DNS_TRUST_PENDING(val->frdataset.trust) ||
DNS_TRUST_ANSWER(val->frdataset.trust)) &&
dns_rdataset_isassociated(&val->fsigrdataset))
if (DNS_TRUST_PENDING(val->frdataset.trust) ||
DNS_TRUST_ANSWER(val->frdataset.trust))
{
/*
* We know the key but haven't validated it yet or
* we have a key of trust answer but a DS
* record for the zone may have been added.
* We know the key but haven't validated it yet, or
* we had a key with trust level "answer" and
* a DS record for the zone has now been added.
*/
result = create_validator(
val, &siginfo->signer, dns_rdatatype_dnskey,
@ -1143,12 +1156,6 @@ seek_dnskey(dns_validator_t *val) {
return result;
}
return DNS_R_WAIT;
} else if (DNS_TRUST_PENDING(val->frdataset.trust)) {
/*
* Having a pending key with no signature means that
* something is broken.
*/
result = DNS_R_CONTINUE;
} else if (val->frdataset.trust < dns_trust_secure) {
/*
* The key is legitimately insecure. There's no
@ -1906,9 +1913,8 @@ get_dsset(dns_validator_t *val, dns_name_t *tname, isc_result_t *resp) {
* We have a DS RRset.
*/
val->dsset = &val->frdataset;
if ((DNS_TRUST_PENDING(val->frdataset.trust) ||
DNS_TRUST_ANSWER(val->frdataset.trust)) &&
dns_rdataset_isassociated(&val->fsigrdataset))
if (DNS_TRUST_PENDING(val->frdataset.trust) ||
DNS_TRUST_ANSWER(val->frdataset.trust))
{
/*
* ... which is signed but not yet validated.
@ -1916,21 +1922,12 @@ get_dsset(dns_validator_t *val, dns_name_t *tname, isc_result_t *resp) {
result = create_validator(
val, tname, dns_rdatatype_ds, &val->frdataset,
&val->fsigrdataset, validator_callback_ds,
"validate_dnskey");
"get_dsset");
*resp = DNS_R_WAIT;
if (result != ISC_R_SUCCESS) {
*resp = result;
}
return ISC_R_COMPLETE;
} else if (DNS_TRUST_PENDING(val->frdataset.trust)) {
/*
* There should never be an unsigned DS.
*/
disassociate_rdatasets(val);
validator_log(val, ISC_LOG_DEBUG(2),
"unsigned DS record");
*resp = DNS_R_NOVALIDSIG;
return ISC_R_COMPLETE;
}
break;
@ -3006,7 +3003,7 @@ seek_ds(dns_validator_t *val, isc_result_t *resp) {
val, ISC_LOG_DEBUG(3),
"no supported algorithm/digest (%s/DS)",
namebuf);
*resp = markanswer(val, "proveunsecure (5)");
*resp = markanswer(val, "seek_ds (1)");
return ISC_R_COMPLETE;
}
@ -3016,22 +3013,12 @@ seek_ds(dns_validator_t *val, isc_result_t *resp) {
/*
* Otherwise, try to validate it now.
*/
if (dns_rdataset_isassociated(&val->fsigrdataset)) {
result = create_validator(
val, tname, dns_rdatatype_ds, &val->frdataset,
&val->fsigrdataset, validator_callback_ds,
"proveunsecure");
*resp = DNS_R_WAIT;
if (result != ISC_R_SUCCESS) {
*resp = result;
}
} else {
/*
* There should never be an unsigned DS.
*/
validator_log(val, ISC_LOG_DEBUG(3),
"unsigned DS record");
*resp = DNS_R_NOVALIDSIG;
result = create_validator(val, tname, dns_rdatatype_ds,
&val->frdataset, &val->fsigrdataset,
validator_callback_ds, "seek_ds");
*resp = DNS_R_WAIT;
if (result != ISC_R_SUCCESS) {
*resp = result;
}
return ISC_R_COMPLETE;
@ -3042,7 +3029,7 @@ seek_ds(dns_validator_t *val, isc_result_t *resp) {
*/
*resp = DNS_R_WAIT;
result = create_fetch(val, tname, dns_rdatatype_ds,
fetch_callback_ds, "proveunsecure");
fetch_callback_ds, "seek_ds");
if (result != ISC_R_SUCCESS) {
*resp = result;
}
@ -3063,7 +3050,7 @@ seek_ds(dns_validator_t *val, isc_result_t *resp) {
result = create_validator(
val, tname, dns_rdatatype_ds, &val->frdataset,
&val->fsigrdataset, validator_callback_ds,
"proveunsecure");
"seek_ds");
*resp = DNS_R_WAIT;
if (result != ISC_R_SUCCESS) {
*resp = result;
@ -3083,7 +3070,7 @@ seek_ds(dns_validator_t *val, isc_result_t *resp) {
NULL) == ISC_R_SUCCESS &&
dns_name_equal(tname, found))
{
*resp = markanswer(val, "proveunsecure (3)");
*resp = markanswer(val, "seek_ds (2)");
return ISC_R_COMPLETE;
}
@ -3102,7 +3089,7 @@ seek_ds(dns_validator_t *val, isc_result_t *resp) {
}
if (isdelegation(tname, &val->frdataset, result)) {
*resp = markanswer(val, "proveunsecure (4)");
*resp = markanswer(val, "seek_ds (3)");
return ISC_R_COMPLETE;
}
@ -3133,7 +3120,7 @@ seek_ds(dns_validator_t *val, isc_result_t *resp) {
result = create_validator(
val, tname, dns_rdatatype_ds, &val->frdataset,
&val->fsigrdataset, validator_callback_ds,
"proveunsecure");
"seek_ds");
if (result != ISC_R_SUCCESS) {
*resp = result;
}
@ -3162,9 +3149,7 @@ seek_ds(dns_validator_t *val, isc_result_t *resp) {
result = create_validator(
val, tname, dns_rdatatype_cname,
&val->frdataset, &val->fsigrdataset,
validator_callback_cname,
"proveunsecure "
"(cname)");
validator_callback_cname, "seek_ds (cname)");
*resp = DNS_R_WAIT;
if (result != ISC_R_SUCCESS) {
*resp = result;