From 5e704bbb5922b5317f362129b1ea9e768c8021a9 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Fri, 5 Dec 2025 17:01:00 +0100 Subject: [PATCH 1/3] Add NSEC3 optout large delegation zone test case This test signs a large delegation with mostly insecure delegations with NSEC3 optout. Once the NSEC3PARAM record is published, run dnssec-verify to ensure the zone is correctly signed. --- bin/tests/system/optout/ns2/controls.conf.j2 | 1 + bin/tests/system/optout/ns2/named.conf.j2 | 41 +++++++ bin/tests/system/optout/ns2/test.db | 22 ++++ bin/tests/system/optout/setup.sh | 14 +++ bin/tests/system/optout/tests_optout.py | 108 +++++++++++++++++++ 5 files changed, 186 insertions(+) create mode 120000 bin/tests/system/optout/ns2/controls.conf.j2 create mode 100644 bin/tests/system/optout/ns2/named.conf.j2 create mode 100644 bin/tests/system/optout/ns2/test.db create mode 100644 bin/tests/system/optout/setup.sh create mode 100755 bin/tests/system/optout/tests_optout.py diff --git a/bin/tests/system/optout/ns2/controls.conf.j2 b/bin/tests/system/optout/ns2/controls.conf.j2 new file mode 120000 index 0000000000..f1371a044e --- /dev/null +++ b/bin/tests/system/optout/ns2/controls.conf.j2 @@ -0,0 +1 @@ +../../_common/controls.conf.in \ No newline at end of file diff --git a/bin/tests/system/optout/ns2/named.conf.j2 b/bin/tests/system/optout/ns2/named.conf.j2 new file mode 100644 index 0000000000..4d9aed3ed0 --- /dev/null +++ b/bin/tests/system/optout/ns2/named.conf.j2 @@ -0,0 +1,41 @@ +/* + * 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. + */ + +options { + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.2; }; + listen-on-v6 { none; }; + allow-transfer { any; }; + recursion no; + dnssec-validation no; + ixfr-from-differences yes; + sig-signing-nodes 900; + sig-signing-signatures 900; +}; + +include "controls.conf"; + +dnssec-policy "optout" { + keys { + csk lifetime unlimited algorithm ecdsa256; + }; + nsec3param iterations 0 optout yes salt-length 0; +}; + +zone "test" { + type primary; + file "test.db"; + dnssec-policy "optout"; + inline-signing yes; +}; diff --git a/bin/tests/system/optout/ns2/test.db b/bin/tests/system/optout/ns2/test.db new file mode 100644 index 0000000000..d3a930229f --- /dev/null +++ b/bin/tests/system/optout/ns2/test.db @@ -0,0 +1,22 @@ +; 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 3600 +@ IN SOA ns2.test. hostmaster.test. 1 7200 3600 24796800 3600 + IN NS ns2 + +ns2 IN A 10.53.0.2 + +a IN A 127.0.0.1 + +$GENERATE 1-50000 child$ IN NS ns.example. + +child303 IN DS 7250 13 2 A30B3F78B6DDE9A4A9A2AD0C805518B4F49EC62E7D3F4531D33DE697 CDA01CB2 diff --git a/bin/tests/system/optout/setup.sh b/bin/tests/system/optout/setup.sh new file mode 100644 index 0000000000..bb08b9c092 --- /dev/null +++ b/bin/tests/system/optout/setup.sh @@ -0,0 +1,14 @@ +#!/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. + +. ../conf.sh diff --git a/bin/tests/system/optout/tests_optout.py b/bin/tests/system/optout/tests_optout.py new file mode 100755 index 0000000000..67628c20e8 --- /dev/null +++ b/bin/tests/system/optout/tests_optout.py @@ -0,0 +1,108 @@ +#!/usr/bin/python3 + +# 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. + + +import os +import re +import sys + +import isctest +import pytest + +pytest.importorskip("dns", minversion="2.0.0") +import dns.exception +import dns.message +import dns.name +import dns.query +import dns.rcode +import dns.rdataclass +import dns.rdatatype + + +pytestmark = [ + pytest.mark.skipif( + sys.version_info < (3, 7), reason="Python >= 3.7 required [GL #3001]" + ), + pytest.mark.extra_artifacts( + [ + "*.out", + "ns2/*.infile", + "ns2/*.signed", + "ns2/*.jnl", + "ns2/*.jbk", + "ns2/controls.conf", + "ns2/dsset-*", + "ns2/K*", + ] + ), +] + + +def has_nsec3param(zone, response): + match = rf"{re.escape(zone)}\.\s+\d+\s+IN\s+NSEC3PARAM\s+1\s+0\s+0\s+-" + + for rr in response.answer: + if re.search(match, rr.to_text()): + return True + + return False + + +def do_query(server, qname, qtype, tcp=False): + msg = isctest.query.create(qname, qtype) + query_func = isctest.query.tcp if tcp else isctest.query.udp + response = query_func(msg, server.ip, expected_rcode=dns.rcode.NOERROR) + return response + + +def do_xfr(server, qname): + xfr = dns.zone.Zone(origin=f"{qname}.", relativize=False) + dns.query.inbound_xfr( + where=server.ip, txn_manager=xfr, port=int(os.environ["PORT"]) + ) + return xfr + + +def verify_zone(zone, transfer): + verify = os.getenv("VERIFY") + assert verify is not None + + filename = f"{zone}.out" + with open(filename, "w", encoding="utf-8") as file: + file.write(transfer.to_text()) + + # dnssec-verify command with default arguments. + verify_cmd = [verify, "-z", "-o", zone, filename] + + verifier = isctest.run.cmd(verify_cmd) + + if verifier.rc != 0: + isctest.log.error(f"dnssec-verify {zone} failed") + + return verifier.rc == 0 + + +def test_optout(ns2): + zone = "test" + + # Wait until the provided zone is signed and then verify its DNSSEC data. + def check_nsec3param(): + response = do_query(ns2, zone, "NSEC3PARAM") + return has_nsec3param(zone, response) + + # check zone is fully signed. + isctest.run.retry_with_timeout(check_nsec3param, timeout=300) + + # check if zone if DNSSEC valid. + transfer = do_xfr(ns2, zone) + assert verify_zone(zone, transfer) From 41159e9062b40495aca6178da54d60bbd2f34378 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Tue, 9 Dec 2025 12:37:20 +0100 Subject: [PATCH 2/3] Implement dns_dbiterator_seek3 This is a new seek function for dbiterator that is meant to find an NSEC3 node in a zone database. The difference with dns_dbiterator_seek is that if the node does not exist, this seek function will point the iterator to the next NSEC3 name. --- lib/dns/dbiterator.c | 8 +++++ lib/dns/include/dns/dbiterator.h | 26 +++++++++++++++ lib/dns/qpcache.c | 16 +++++++-- lib/dns/qpzone.c | 57 ++++++++++++++++++++++++++++++-- lib/dns/sdlz.c | 16 +++++++-- tests/dns/dbiterator_test.c | 32 +++++++++++++----- 6 files changed, 137 insertions(+), 18 deletions(-) diff --git a/lib/dns/dbiterator.c b/lib/dns/dbiterator.c index 626e4c4543..4732d706c8 100644 --- a/lib/dns/dbiterator.c +++ b/lib/dns/dbiterator.c @@ -58,6 +58,14 @@ dns__dbiterator_seek(dns_dbiterator_t *iterator, return iterator->methods->seek(iterator, name DNS__DB_FLARG_PASS); } +isc_result_t +dns__dbiterator_seek3(dns_dbiterator_t *iterator, + const dns_name_t *name DNS__DB_FLARG) { + REQUIRE(DNS_DBITERATOR_VALID(iterator)); + + return iterator->methods->seek3(iterator, name DNS__DB_FLARG_PASS); +} + isc_result_t dns__dbiterator_prev(dns_dbiterator_t *iterator DNS__DB_FLARG) { REQUIRE(DNS_DBITERATOR_VALID(iterator)); diff --git a/lib/dns/include/dns/dbiterator.h b/lib/dns/include/dns/dbiterator.h index 0371401cb2..c3f92cd687 100644 --- a/lib/dns/include/dns/dbiterator.h +++ b/lib/dns/include/dns/dbiterator.h @@ -70,6 +70,8 @@ typedef struct dns_dbiteratormethods { isc_result_t (*last)(dns_dbiterator_t *iterator DNS__DB_FLARG); isc_result_t (*seek)(dns_dbiterator_t *iterator, const dns_name_t *name DNS__DB_FLARG); + isc_result_t (*seek3)(dns_dbiterator_t *iterator, + const dns_name_t *name DNS__DB_FLARG); isc_result_t (*prev)(dns_dbiterator_t *iterator DNS__DB_FLARG); isc_result_t (*next)(dns_dbiterator_t *iterator DNS__DB_FLARG); isc_result_t (*current)(dns_dbiterator_t *iterator, @@ -189,6 +191,30 @@ dns__dbiterator_seek(dns_dbiterator_t *iterator, *\li Other results are possible, depending on the DB implementation. */ +#define dns_dbiterator_seek3(iterator, name) \ + dns__dbiterator_seek3(iterator, name DNS__DB_FILELINE) +isc_result_t +dns__dbiterator_seek3(dns_dbiterator_t *iterator, + const dns_name_t *name DNS__DB_FLARG); +/*%< + * Move the node cursor to the node with NSEC3 name 'name'. + * If not found, the iterator is set to the next name. + * + * Requires: + *\li 'iterator' is a valid iterator. + * + *\li 'name' is a valid name. + * + * Returns: + *\li #ISC_R_SUCCESS + *\li #ISC_R_NOTFOUND + *\li #ISC_R_NOMORE There are no NSEC3 nodes in the database. + *\li #ISC_R_NOTIMPLEMENTED + * (this function is only implemented for NSEC3 only iterators) + * + *\li Other results are possible, depending on the DB implementation. + */ + #define dns_dbiterator_prev(iterator) \ dns__dbiterator_prev(iterator DNS__DB_FILELINE) isc_result_t diff --git a/lib/dns/qpcache.c b/lib/dns/qpcache.c index 50218a3e25..94a7c8aed5 100644 --- a/lib/dns/qpcache.c +++ b/lib/dns/qpcache.c @@ -374,6 +374,9 @@ static isc_result_t dbiterator_seek(dns_dbiterator_t *iterator, const dns_name_t *name DNS__DB_FLARG); static isc_result_t +dbiterator_seek3(dns_dbiterator_t *iterator, + const dns_name_t *name DNS__DB_FLARG); +static isc_result_t dbiterator_prev(dns_dbiterator_t *iterator DNS__DB_FLARG); static isc_result_t dbiterator_next(dns_dbiterator_t *iterator DNS__DB_FLARG); @@ -386,9 +389,10 @@ static isc_result_t dbiterator_origin(dns_dbiterator_t *iterator, dns_name_t *name); static dns_dbiteratormethods_t dbiterator_methods = { - dbiterator_destroy, dbiterator_first, dbiterator_last, - dbiterator_seek, dbiterator_prev, dbiterator_next, - dbiterator_current, dbiterator_pause, dbiterator_origin + dbiterator_destroy, dbiterator_first, dbiterator_last, + dbiterator_seek, dbiterator_seek3, dbiterator_prev, + dbiterator_next, dbiterator_current, dbiterator_pause, + dbiterator_origin }; /* @@ -3634,6 +3638,12 @@ dbiterator_seek(dns_dbiterator_t *iterator, return result; } +static isc_result_t +dbiterator_seek3(dns_dbiterator_t *iterator ISC_ATTR_UNUSED, + const dns_name_t *name ISC_ATTR_UNUSED DNS__DB_FLARG) { + return ISC_R_NOTIMPLEMENTED; +} + static isc_result_t dbiterator_prev(dns_dbiterator_t *iterator ISC_ATTR_UNUSED DNS__DB_FLARG) { return ISC_R_NOTIMPLEMENTED; diff --git a/lib/dns/qpzone.c b/lib/dns/qpzone.c index fadcd0996d..9cd5c77c62 100644 --- a/lib/dns/qpzone.c +++ b/lib/dns/qpzone.c @@ -345,6 +345,9 @@ static isc_result_t dbiterator_seek(dns_dbiterator_t *iterator, const dns_name_t *name DNS__DB_FLARG); static isc_result_t +dbiterator_seek3(dns_dbiterator_t *iterator, + const dns_name_t *name DNS__DB_FLARG); +static isc_result_t dbiterator_prev(dns_dbiterator_t *iterator DNS__DB_FLARG); static isc_result_t dbiterator_next(dns_dbiterator_t *iterator DNS__DB_FLARG); @@ -357,9 +360,10 @@ static isc_result_t dbiterator_origin(dns_dbiterator_t *iterator, dns_name_t *name); static dns_dbiteratormethods_t dbiterator_methods = { - dbiterator_destroy, dbiterator_first, dbiterator_last, - dbiterator_seek, dbiterator_prev, dbiterator_next, - dbiterator_current, dbiterator_pause, dbiterator_origin + dbiterator_destroy, dbiterator_first, dbiterator_last, + dbiterator_seek, dbiterator_seek3, dbiterator_prev, + dbiterator_next, dbiterator_current, dbiterator_pause, + dbiterator_origin }; typedef struct qpdb_dbiterator { @@ -4357,6 +4361,53 @@ dbiterator_seek(dns_dbiterator_t *iterator, return result; } +static isc_result_t +dbiterator_seek3(dns_dbiterator_t *iterator, + const dns_name_t *name DNS__DB_FLARG) { + isc_result_t result; + qpdb_dbiterator_t *qpdbiter = (qpdb_dbiterator_t *)iterator; + + if (qpdbiter->result != ISC_R_SUCCESS && + qpdbiter->result != ISC_R_NOTFOUND && + qpdbiter->result != DNS_R_PARTIALMATCH && + qpdbiter->result != ISC_R_NOMORE) + { + return qpdbiter->result; + } + + if (qpdbiter->nsec3mode != nsec3only) { + return ISC_R_NOTIMPLEMENTED; + } + + dereference_iter_node(qpdbiter DNS__DB_FLARG_PASS); + + result = dns_qp_lookup(qpdbiter->snap, name, DNS_DBNAMESPACE_NSEC3, + &qpdbiter->iter, NULL, (void **)&qpdbiter->node, + NULL); + + switch (result) { + case ISC_R_SUCCESS: + reference_iter_node(qpdbiter DNS__DB_FLARG_PASS); + break; + case DNS_R_PARTIALMATCH: + /* dbiterator_next() will dereference the node */ + reference_iter_node(qpdbiter DNS__DB_FLARG_PASS); + + result = dbiterator_next(iterator); + if (result == ISC_R_NOMORE) { + result = dbiterator_first(iterator); + } + break; + case ISC_R_NOTFOUND: + default: + break; + } + + qpdbiter->result = result; + + return qpdbiter->result; +} + static isc_result_t dbiterator_prev(dns_dbiterator_t *iterator DNS__DB_FLARG) { isc_result_t result; diff --git a/lib/dns/sdlz.c b/lib/dns/sdlz.c index 22bd0ee210..aa6e58305c 100644 --- a/lib/dns/sdlz.c +++ b/lib/dns/sdlz.c @@ -210,6 +210,9 @@ static isc_result_t dbiterator_seek(dns_dbiterator_t *iterator, const dns_name_t *name DNS__DB_FLARG); static isc_result_t +dbiterator_seek3(dns_dbiterator_t *iterator, + const dns_name_t *name DNS__DB_FLARG); +static isc_result_t dbiterator_prev(dns_dbiterator_t *iterator DNS__DB_FLARG); static isc_result_t dbiterator_next(dns_dbiterator_t *iterator DNS__DB_FLARG); @@ -222,9 +225,10 @@ static isc_result_t dbiterator_origin(dns_dbiterator_t *iterator, dns_name_t *name); static dns_dbiteratormethods_t dbiterator_methods = { - dbiterator_destroy, dbiterator_first, dbiterator_last, - dbiterator_seek, dbiterator_prev, dbiterator_next, - dbiterator_current, dbiterator_pause, dbiterator_origin + dbiterator_destroy, dbiterator_first, dbiterator_last, + dbiterator_seek, dbiterator_seek3, dbiterator_prev, + dbiterator_next, dbiterator_current, dbiterator_pause, + dbiterator_origin }; /* @@ -1149,6 +1153,12 @@ dbiterator_seek(dns_dbiterator_t *iterator, return ISC_R_NOTFOUND; } +static isc_result_t +dbiterator_seek3(dns_dbiterator_t *iterator ISC_ATTR_UNUSED, + const dns_name_t *name ISC_ATTR_UNUSED DNS__DB_FLARG) { + return ISC_R_NOTIMPLEMENTED; +} + static isc_result_t dbiterator_prev(dns_dbiterator_t *iterator DNS__DB_FLARG) { sdlz_dbiterator_t *sdlziter = (sdlz_dbiterator_t *)iterator; diff --git a/tests/dns/dbiterator_test.c b/tests/dns/dbiterator_test.c index a4593bf945..c15e460827 100644 --- a/tests/dns/dbiterator_test.c +++ b/tests/dns/dbiterator_test.c @@ -182,10 +182,10 @@ ISC_RUN_TEST_IMPL(reverse_nsec3) { /* seek: walk database starting at a particular node */ static void -test_seek_node(const char *filename, int flags, int nodes) { - isc_result_t result; +test_seek_node(const char *filename, bool nsec3, int flags, int nodes) { + isc_result_t result, result3; dns_db_t *db = NULL; - dns_dbiterator_t *iter = NULL; + dns_dbiterator_t *iter = NULL, *iter3 = NULL; dns_dbnode_t *node = NULL; dns_name_t *name, *seekname; dns_fixedname_t f1, f2; @@ -200,6 +200,9 @@ test_seek_node(const char *filename, int flags, int nodes) { result = dns_db_createiterator(db, flags, &iter); assert_int_equal(result, ISC_R_SUCCESS); + result3 = dns_db_createiterator(db, flags, &iter3); + assert_int_equal(result3, ISC_R_SUCCESS); + result = make_name("c." TEST_ORIGIN, seekname); assert_int_equal(result, ISC_R_SUCCESS); @@ -207,6 +210,14 @@ test_seek_node(const char *filename, int flags, int nodes) { if (flags == DNS_DB_NSEC3ONLY) { /* "c" isn't in the NSEC3 tree but the origin node is */ assert_int_equal(result, DNS_R_PARTIALMATCH); + + /* NSEC3 iterator */ + result3 = dns_dbiterator_seek3(iter3, seekname); + if (nsec3) { + assert_int_equal(result3, ISC_R_SUCCESS); + } else { + assert_int_equal(result3, ISC_R_NOMORE); + } } else { assert_int_equal(result, ISC_R_SUCCESS); } @@ -244,26 +255,29 @@ test_seek_node(const char *filename, int flags, int nodes) { assert_int_equal(i, nodes); dns_dbiterator_destroy(&iter); + dns_dbiterator_destroy(&iter3); dns_db_detach(&db); } ISC_RUN_TEST_IMPL(seek_node) { UNUSED(state); - test_seek_node(TESTS_DIR "/testdata/dbiterator/zone1.data", 0, 9); - test_seek_node(TESTS_DIR "/testdata/dbiterator/zone1.data", + test_seek_node(TESTS_DIR "/testdata/dbiterator/zone1.data", false, 0, + 9); + test_seek_node(TESTS_DIR "/testdata/dbiterator/zone1.data", false, DNS_DB_NONSEC3, 9); - test_seek_node(TESTS_DIR "/testdata/dbiterator/zone1.data", + test_seek_node(TESTS_DIR "/testdata/dbiterator/zone1.data", false, DNS_DB_NSEC3ONLY, 0); } ISC_RUN_TEST_IMPL(seek_node_nsec3) { UNUSED(state); - test_seek_node(TESTS_DIR "/testdata/dbiterator/zone2.data", 0, 29); - test_seek_node(TESTS_DIR "/testdata/dbiterator/zone2.data", + test_seek_node(TESTS_DIR "/testdata/dbiterator/zone2.data", true, 0, + 29); + test_seek_node(TESTS_DIR "/testdata/dbiterator/zone2.data", true, DNS_DB_NONSEC3, 9); - test_seek_node(TESTS_DIR "/testdata/dbiterator/zone2.data", + test_seek_node(TESTS_DIR "/testdata/dbiterator/zone2.data", true, DNS_DB_NSEC3ONLY, 0); } From 6f7abbfaac1aad2716532c83d2dff6dd804b053b Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Tue, 9 Dec 2025 13:45:40 +0100 Subject: [PATCH 3/3] Don't create NSEC3 nodes for unsigned delegations Instead of creating new nodes for every possible NSEC3 record, only create them if we are actually going to add a new NSEC3 record. --- lib/dns/nsec3.c | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/dns/nsec3.c b/lib/dns/nsec3.c index ff53e90be9..bf9c8486ac 100644 --- a/lib/dns/nsec3.c +++ b/lib/dns/nsec3.c @@ -565,7 +565,24 @@ dns_nsec3_addnsec3(dns_db_t *db, dns_dbversion_t *version, * Create the node if it doesn't exist and hold * a reference to it until we have added the NSEC3. */ - CHECK(dns_db_findnsec3node(db, hashname, true, &newnode)); + result = dns_db_findnsec3node(db, hashname, false, &newnode); + if (result != ISC_R_SUCCESS) { + isc_result_t tresult; + + CHECK(dns_db_createiterator(db, DNS_DB_NSEC3ONLY, &dbit)); + tresult = dns_dbiterator_seek3(dbit, hashname); + CHECK(dns_dbiterator_pause(dbit)); + + if (tresult != ISC_R_SUCCESS) { + /* Nothing in the NSEC3 space yet. */ + if (!unsecure) { + goto addnsec3; + } + goto cleanup; + } + + goto find_previous; + } /* * Seek the iterator to the 'newnode'. @@ -610,6 +627,7 @@ dns_nsec3_addnsec3(dns_db_t *db, dns_dbversion_t *version, } } +find_previous: /* * Find the previous NSEC3 (if any) and update it if required. */ @@ -694,6 +712,10 @@ addnsec3: /* * Create the NSEC3 RDATA. */ + if (newnode == NULL) { + CHECK(dns_db_findnsec3node(db, hashname, true, &newnode)); + } + CHECK(dns_db_findnode(db, name, false, &node)); CHECK(dns_nsec3_buildrdata(db, version, node, hash, flags, iterations, salt, salt_length, nexthash, next_length,