Compare commits

...

3 Commits

Author SHA1 Message Date
Colin Vidal
19ace9e681 fixup! add system tests covering EDE 9 2025-03-20 10:57:28 +01:00
Colin Vidal
e2b027001b add system tests covering EDE 9
Add DNSSEC system tests covering extended DNS error 9. Because BIND9 won't
returned signed records (RRSIG) when provided a signed zone without DNSKEY
records, this test involves a new mock DNS server which loads a
fully signed zone and return associated RRSIG to its RR, but also return NODATA
when the query type is DNSKEY. This enable to run a resolver trying to validate
a domain, but failing because it can't get the DNSKEY, thus expecting the EDE 9
message.
2025-03-19 17:47:48 +01:00
Colin Vidal
c9b7857387 add support for EDE 9
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.

The EDE 9 is sent only in the proveunsecure flow because we want to
send it only in the case there has to be a validation, so we need to
make sure the parent domain has a DS record to the children.
2025-03-19 17:47:48 +01:00
9 changed files with 136 additions and 1 deletions

View File

@@ -30,3 +30,6 @@ ns8 is a caching-only server, configured with unsupported and disabled
algorithms. It is used for testing failure cases.
ns9 is a forwarding-only server.
ans11 is a mocked authoritative server. Used to simulate broken authoritative
server supporting DNSSEC.

View File

@@ -0,0 +1,54 @@
# 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.
from isctest.asyncserver import (
AsyncDnsServer,
DomainHandler,
QueryContext,
ResponseAction,
DnsResponseSend,
)
from typing import AsyncGenerator
import dns
class DnssecManglerHandler(DomainHandler):
domains = ["missingdnskey.example."]
def hide_dnskey(self, qctx: QueryContext) -> None:
qctx.response.answer = []
def add_rrsig(self, qctx: QueryContext) -> None:
rrsig = None
if qctx.qname == qctx.zone.origin:
rrsig = qctx.zone.find_rdataset(
qctx.zone.origin, dns.rdatatype.RRSIG, qctx.qtype, True
)
else:
rrsig = qctx.node.get_rdataset(qctx.qclass, dns.rdatatype.RRSIG, True)
if rrsig:
rrsig_rrset = dns.rrset.RRset(qctx.qname, qctx.qclass, dns.rdatatype.RRSIG)
rrsig_rrset.update(rrsig)
qctx.response.answer.append(rrsig_rrset)
async def get_responses(
self, qctx: QueryContext
) -> AsyncGenerator[ResponseAction, None]:
if qctx.qtype == dns.rdatatype.DNSKEY:
self.hide_dnskey(qctx)
else:
self.add_rrsig(qctx)
yield DnsResponseSend(qctx.response)
server = AsyncDnsServer()
server.install_response_handler(DnssecManglerHandler())
server.run()

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 ; 5 minutes
@ 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.3
a A 10.0.0.1

View File

@@ -0,0 +1,31 @@
#!/bin/sh -e
# 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.
# shellcheck source=conf.sh
. ../../conf.sh
set -e
echo_i "ans11/sign.sh"
zone=missingdnskey.example.
infile=missingdnskey.example.db.in
zonefile=missingdnskey.example.db
keyname1=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -n zone "$zone")
keyname2=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -n zone -f KSK "$zone")
cat "$infile" "$keyname1.key" "$keyname2.key" >"$zonefile"
"$SIGNER" -o "$zone" "$zonefile" >/dev/null
# AsyncDnsServer expects a file zone name defined as <apexname>.db, so let's
# renamed the signed file.
mv "$zonefile".signed $zonefile

View File

@@ -177,3 +177,6 @@ rsasha1-1024 NS ns.rsasha1-1024
ns.rsasha1-1024 A 10.53.0.3
dname-at-apex-nsec3 NS ns3
missingdnskey NS ns.missingdnskey
ns.missingdnskey A 10.53.0.11

View File

@@ -16,8 +16,9 @@
set -e
# Sign child zones (served by ns3).
# Sign child zones (served by ns3 and ans11).
(cd ../ns3 && $SHELL sign.sh)
(cd ../ans11 && $SHELL sign.sh)
echo_i "ns2/sign.sh"
@@ -69,6 +70,8 @@ for subdomain in digest-alg-unsupported ds-unsupported secure badds \
cp "../ns3/dsset-$subdomain.example." .
done
cp "../ans11/dsset-missingdnskey.example." .
# Sign the "example." zone.
keyname1=$("$KEYGEN" -q -a "$ALTERNATIVE_ALGORITHM" -b "$ALTERNATIVE_BITS" -n zone -f KSK "$zone")
keyname2=$("$KEYGEN" -q -a "$ALTERNATIVE_ALGORITHM" -b "$ALTERNATIVE_BITS" -n zone "$zone")

View File

@@ -4133,6 +4133,16 @@ n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check EDE 9 (Missing DNSKEY) is added when the zone is signed but no DNSKEY is provided ($n)"
ret=0
dig_with_opts @10.53.0.4 missingdnskey.example SOA >dig.out.ns4.test$n
grep "status: SERVFAIL," dig.out.ns4.test$n >/dev/null || ret=1
grep "flags:.*ad.*QUERY" dig.out.ns4.test$n >/dev/null && ret=1
grep "; EDE: 9 (DNSKEY Missing): (missingdnskey.example/DNSKEY)" dig.out.ns4.test$n >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
# The next two queries ensure that a zone signed with a DNSKEY that the nameserver
# has a disabled algorithm match for will yield insecure positive responses.
# These trust anchors in ns8 are ignored and so this domain is treated as insecure.

View File

@@ -156,6 +156,8 @@ pytestmark = pytest.mark.extra_artifacts(
"ns6/optout-tld.db",
"ns7/split-rrsig.db",
"ns7/split-rrsig.db.unsplit",
"ans11/missingdnskey.example.db",
"ans11/missingdnskey.example.db.tmp",
"signer/example.db",
"signer/example.db.after",
"signer/example.db.before",

View File

@@ -3275,6 +3275,11 @@ proveunsecure(dns_validator_t *val, bool have_ds, bool resume) {
/* 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, NULL);
}
return DNS_R_NOTINSECURE;
out: