new: usr: Add support for Extended DNS Error 9 (Missing DNSKEY)

Extended DNS Error 9 (Missing DNSKEY) is now sent when a validating resolver attempts to validate a response but can't get the DNSKEY from the authoritative server of the zone, while the DS record is present in the parent zone.

See #2715

Merge branch '2715-missingdnskey' into 'main'

See merge request isc-projects/bind9!10296
This commit is contained in:
Colin Vidal 2025-12-17 10:18:53 +01:00
commit fe456b47f9
11 changed files with 163 additions and 1 deletions

View file

@ -47,3 +47,9 @@ inconsistent. NS ns2.inconsistent.
ns2.inconsistent. A 10.53.0.2
nsec-rrsigs-stripped. NS ns10.nsec-rrsigs-stripped.
ns10.nsec-rrsigs-stripped. A 10.53.0.10
ns.missing-dnskey. A 10.53.0.2
missing-dnskey. NS ns.missing-dnskey.
ns.missing-ksk. A 10.53.0.2
missing-ksk. NS ns.missing-ksk.
ns.wrong-dnskey. A 10.53.0.2
wrong-dnskey. NS ns.wrong-dnskey.

View file

@ -33,6 +33,9 @@ cp "../ns2/dsset-peer-ns-spoof." .
cp "../ns2/dsset-dnskey-rrsigs-stripped." .
cp "../ns2/dsset-ds-rrsigs-stripped." .
cp "../ns2/dsset-inconsistent." .
cp "../ns2/dsset-missing-dnskey." .
cp "../ns2/dsset-wrong-dnskey." .
cp "../ns2/dsset-missing-ksk." .
grep "$DEFAULT_ALGORITHM_NUMBER [12] " "../ns2/dsset-algroll." >"dsset-algroll."
cp "../ns6/dsset-optout-tld." .

View file

@ -0,0 +1,24 @@
; 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
@ IN SOA mname1. . (
2000042407 ; serial
20 ; refresh (20 seconds)
20 ; retry (20 seconds)
1814400 ; expire (3 weeks)
3600 ; minimum (1 hour)
)
NS ns
MX 10 mx
ns A 10.53.0.2
a A 10.0.0.1
b A 10.0.0.2

View file

@ -0,0 +1,24 @@
; 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
@ IN SOA mname1. . (
2000042407 ; serial
20 ; refresh (20 seconds)
20 ; retry (20 seconds)
1814400 ; expire (3 weeks)
3600 ; minimum (1 hour)
)
NS ns
MX 10 mx
ns A 10.53.0.2
a A 10.0.0.1
b A 10.0.0.2

View file

@ -239,4 +239,19 @@ zone "child.ds-rrsigs-stripped" {
file "child.ds-rrsigs-stripped.db.signed";
};
zone "missing-dnskey" {
type primary;
file "missing-dnskey.db.signed";
};
zone "missing-ksk" {
type primary;
file "missing-ksk.db.signed";
};
zone "wrong-dnskey" {
type primary;
file "wrong-dnskey.db.signed";
};
include "trusted.conf";

View file

@ -469,3 +469,40 @@ key1=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -f KSK "$zone")
key2=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone")
cat "$infile" "$key1.key" "$key2.key" >"$zonefile"
"$SIGNER" -3 - -g -o "$zone" "$zonefile" >/dev/null 2>&1
#
# The DNSKEYs gets removed from the signed zone.
#
zone=missing-dnskey
infile=missing-dnskey.db.in
zonefile=missing-dnskey.db
key1=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone")
key2=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -f KSK "$zone")
cat "$infile" "$key1.key" "$key2.key" >"$zonefile"
"$SIGNER" -o "$zone" "$zonefile" >/dev/null 2>&1
"$CHECKZONE" -D -q -i local "$zone" "$zonefile.signed" | awk '$4 == "DNSKEY" { next } { print }' >"$zonefile.stripped"
mv "$zonefile.stripped" "$zonefile.signed"
#
# The KSK gets removed from the signed zone, but the ZSK is still there.
# 257 is the flag value indicating the key is the KSK
#
zone=missing-ksk
infile=missing-ksk.db.in
zonefile=missing-ksk.db
key1=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone")
key2=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -f KSK "$zone")
cat "$infile" "$key1.key" "$key2.key" >"$zonefile"
"$SIGNER" -o "$zone" "$zonefile" >/dev/null 2>&1
"$CHECKZONE" -D -q -i local "$zone" "$zonefile.signed" | awk '$4 == "DNSKEY" && $5 == "257" { next } { print }' >"$zonefile.stripped"
mv "$zonefile.stripped" "$zonefile.signed"
zone=wrong-dnskey
infile=wrong-dnskey.db.in
zonefile=wrong-dnskey.db
key1=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone")
key2=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -f KSK "$zone")
cat "$infile" "$key1.key" "$key2.key" >"$zonefile"
"$SIGNER" -o "$zone" "$zonefile" >/dev/null 2>&1
key3=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -f KSK "$zone")
$DSFROMKEY "$key3.key" >"dsset-$zone."

View file

@ -0,0 +1,24 @@
; 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
@ IN SOA mname1. . (
2000042407 ; serial
20 ; refresh (20 seconds)
20 ; retry (20 seconds)
1814400 ; expire (3 weeks)
3600 ; minimum (1 hour)
)
NS ns
MX 10 mx
ns A 10.53.0.2
a A 10.0.0.1
b A 10.0.0.2

View file

@ -1109,6 +1109,22 @@ def test_validating_forwarder(ns4, ns9):
watcher.wait_for_line("status: SERVFAIL")
@pytest.mark.parametrize(
"zone",
[
"missing-dnskey",
"wrong-dnskey",
"missing-ksk",
"a.extradsunknownoid.example",
],
)
def test_missing_dnskey(zone, ns4):
msg = isctest.query.create(f"a.{zone}", "A")
res = isctest.query.tcp(msg, ns4.ip)
isctest.check.servfail(res)
isctest.check.ede(res, EDECode.DNSKEY_MISSING)
def test_expired_signatures(ns4):
# check expired signatures do not validate
msg = isctest.query.create("expired.example", "SOA")

View file

@ -20,6 +20,10 @@ zone=example.
infile=example.db.in
zonefile=example.db
# The zone is signed but it's broken: instead of having a ZSK and a KSK (which
# is the DNSKEY pointed by the parent's DS), it has two ZSKs. As a result,
# `example.` validations will always fail, resulting into a SERVFAIL on
# validating resolvers.
keyname1=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone")
keyname2=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone")

View file

@ -20,9 +20,11 @@ def check_sfcache_ede(ns, ede):
res = isctest.query.udp(msg, ns.ip)
isctest.check.servfail(res)
if ede:
# The SERVFAIL is cached, so now it shows up the EDE CACHED_ERROR, but not the DNSKEY_MISSING.
isctest.check.ede(res, EDECode.CACHED_ERROR)
else:
isctest.check.noede(res)
# example. domain DNSSEC is misconfigured on ns2, as it have two ZSK but no KSK. As a result, the DNSKEY for example. can't be found.
isctest.check.ede(res, EDECode.DNSKEY_MISSING)
def test_sfcache_ede(ns5, templates):

View file

@ -2112,6 +2112,8 @@ validate_dnskey_dsset(dns_validator_t *val) {
&keyrdata);
if (result != ISC_R_SUCCESS) {
validator_log(val, ISC_LOG_DEBUG(3), "no DNSKEY matching DS");
validator_addede(val, DNS_EDE_DNSKEYMISSING,
"DNSKEY found but not matching DS");
return DNS_R_NOKEYMATCH;
}
@ -3522,6 +3524,11 @@ proveunsecure(dns_validator_t *val, bool have_ds, bool have_dnskey,
/* Couldn't complete insecurity proof. */
validator_log(val, ISC_LOG_DEBUG(3), "insecurity proof failed: %s",
isc_result_totext(result));
if (val->type == dns_rdatatype_dnskey && val->rdataset == NULL) {
validator_addede(val, DNS_EDE_DNSKEYMISSING, "no DNSKEY found");
}
return DNS_R_NOTINSECURE;
out: