[9.20] fix: usr: Reconfigure NSEC3 opt-out zone to NSEC causes zone to be invalid

A zone that is signed with NSEC3, opt-out enabled, and then reconfigured to use NSEC, causes the zone to be published with missing NSEC records. This has been fixed.

Closes #5679

Backport of MR !11359

Merge branch 'backport-5679-nsec3-optout-to-nsec-9.20' into 'bind-9.20'

See merge request isc-projects/bind9!11401
This commit is contained in:
Mark Andrews 2025-12-22 16:09:08 +11:00
commit 1d0e19c612
6 changed files with 154 additions and 100 deletions

View file

@ -11,6 +11,9 @@
* information regarding copyright ownership.
*/
{% set reconfiged = reconfiged | default(False) %}
{% set policy = "optout" if not reconfiged else "nsec" %}
options {
port @PORT@;
pid-file "named.pid";
@ -33,9 +36,22 @@ dnssec-policy "optout" {
nsec3param iterations 0 optout yes salt-length 0;
};
dnssec-policy "nsec" {
keys {
csk lifetime unlimited algorithm ecdsa256;
};
};
zone "test" {
type primary;
file "test.db";
dnssec-policy "optout";
inline-signing yes;
};
zone "small.test" {
type primary;
file "small.test.db";
dnssec-policy "@policy@";
inline-signing yes;
};

View file

@ -0,0 +1,25 @@
; 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.small.test. hostmaster.small.test. 1 7200 3600 24796800 3600
IN NS ns2
ns2 IN A 10.53.0.2
a IN A 127.0.0.1
dname IN DNAME branch.example.
under.dname IN TXT "occluded"
$GENERATE 1-10 child$ IN NS ns.example.
child5 IN DS 7250 13 2 A30B3F78B6DDE9A4A9A2AD0C805518B4F49EC62E7D3F4531D33DE697 CDA01CB2

View file

@ -17,6 +17,9 @@ ns2 IN A 10.53.0.2
a IN A 127.0.0.1
dname IN DNAME branch.example.
under.dname IN TXT "occluded"
$GENERATE 1-50000 child$ IN NS ns.example.
child303 IN DS 7250 13 2 A30B3F78B6DDE9A4A9A2AD0C805518B4F49EC62E7D3F4531D33DE697 CDA01CB2

View file

@ -1,14 +0,0 @@
#!/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

View file

@ -94,14 +94,51 @@ def verify_zone(zone, transfer):
def test_optout(ns2):
zone = "test"
expect_nsec3param = True
# 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)
if expect_nsec3param:
return has_nsec3param(zone, response)
return not has_nsec3param(zone, response)
# check zone is fully signed.
isctest.run.retry_with_timeout(check_nsec3param, timeout=300)
isctest.run.retry_with_timeout(check_nsec3param, timeout=100)
# check if zone if DNSSEC valid.
transfer = do_xfr(ns2, zone)
assert verify_zone(zone, transfer)
def test_optout_to_nsec(ns2, templates):
zone = "small.test"
expect_nsec3param = True
# Wait until the provided zone is signed and then verify its DNSSEC data.
def check_nsec3param():
response = do_query(ns2, zone, "NSEC3PARAM")
if expect_nsec3param:
return has_nsec3param(zone, response)
return not has_nsec3param(zone, response)
# check zone is fully signed.
isctest.run.retry_with_timeout(check_nsec3param, timeout=100)
# check if zone if DNSSEC valid.
transfer = do_xfr(ns2, zone)
assert verify_zone(zone, transfer)
# reconfigure to NSEC.
data = {
"reconfiged": True,
}
templates.render(f"{ns2.identifier}/named.conf", data)
ns2.reconfigure()
# wait until NSEC3PARAM is removed.
expect_nsec3param = False
isctest.run.retry_with_timeout(check_nsec3param, timeout=100)
# check if zone if DNSSEC valid.
transfer = do_xfr(ns2, zone)

View file

@ -7627,6 +7627,58 @@ cleanup:
return result;
}
typedef struct seen {
bool rr;
bool soa;
bool ns;
bool nsec;
bool nsec3;
bool ds;
bool dname;
} seen_t;
static isc_result_t
allrdatasets(dns_db_t *db, dns_dbnode_t *node, dns_dbversion_t *version,
dns_rdatasetiter_t **iterp, seen_t *seen) {
isc_result_t result;
dns_rdataset_t rdataset = DNS_RDATASET_INIT;
*seen = (seen_t){};
RETERR(dns_db_allrdatasets(db, node, version, 0, 0, iterp));
for (result = dns_rdatasetiter_first(*iterp); result == ISC_R_SUCCESS;
result = dns_rdatasetiter_next(*iterp))
{
dns_rdatasetiter_current(*iterp, &rdataset);
if (rdataset.type == dns_rdatatype_rrsig) {
dns_rdataset_disassociate(&rdataset);
continue;
}
(*seen).rr = true;
if (rdataset.type == dns_rdatatype_soa) {
(*seen).soa = true;
} else if (rdataset.type == dns_rdatatype_ns) {
(*seen).ns = true;
} else if (rdataset.type == dns_rdatatype_ds) {
(*seen).ds = true;
} else if (rdataset.type == dns_rdatatype_dname) {
(*seen).dname = true;
} else if (rdataset.type == dns_rdatatype_nsec) {
(*seen).nsec = true;
} else if (rdataset.type == dns_rdatatype_nsec3) {
(*seen).nsec3 = true;
}
dns_rdataset_disassociate(&rdataset);
}
return ISC_R_SUCCESS;
}
static isc_result_t
sign_a_node(dns_db_t *db, dns_zone_t *zone, dns_name_t *name,
dns_dbnode_t *node, dns_dbversion_t *version, bool build_nsec3,
@ -7643,13 +7695,13 @@ sign_a_node(dns_db_t *db, dns_zone_t *zone, dns_name_t *name,
bool offlineksk = false;
isc_buffer_t buffer;
unsigned char data[1024];
bool seen_soa, seen_ns, seen_rr, seen_nsec, seen_nsec3, seen_ds;
seen_t seen;
if (zone->kasp != NULL) {
offlineksk = dns_kasp_offlineksk(zone->kasp);
}
result = dns_db_allrdatasets(db, node, version, 0, 0, &iterator);
result = allrdatasets(db, node, version, &iterator, &seen);
if (result != ISC_R_SUCCESS) {
if (result == ISC_R_NOTFOUND) {
result = ISC_R_SUCCESS;
@ -7659,36 +7711,13 @@ sign_a_node(dns_db_t *db, dns_zone_t *zone, dns_name_t *name,
dns_rdataset_init(&rdataset);
isc_buffer_init(&buffer, data, sizeof(data));
seen_rr = seen_soa = seen_ns = seen_nsec = seen_nsec3 = seen_ds = false;
for (result = dns_rdatasetiter_first(iterator); result == ISC_R_SUCCESS;
result = dns_rdatasetiter_next(iterator))
{
dns_rdatasetiter_current(iterator, &rdataset);
if (rdataset.type == dns_rdatatype_soa) {
seen_soa = true;
} else if (rdataset.type == dns_rdatatype_ns) {
seen_ns = true;
} else if (rdataset.type == dns_rdatatype_ds) {
seen_ds = true;
} else if (rdataset.type == dns_rdatatype_nsec) {
seen_nsec = true;
} else if (rdataset.type == dns_rdatatype_nsec3) {
seen_nsec3 = true;
}
if (rdataset.type != dns_rdatatype_rrsig) {
seen_rr = true;
}
dns_rdataset_disassociate(&rdataset);
}
if (result != ISC_R_NOMORE) {
goto cleanup;
}
/*
* Going from insecure to NSEC3.
* Don't generate NSEC3 records for NSEC3 records.
*/
if (build_nsec3 && !seen_nsec3 && seen_rr) {
bool unsecure = !seen_ds && seen_ns && !seen_soa;
if (build_nsec3 && !seen.nsec3 && seen.rr) {
bool unsecure = !seen.ds && seen.ns && !seen.soa;
CHECK(dns_nsec3_addnsec3s(db, version, name, nsecttl, unsecure,
diff));
(*signatures)--;
@ -7697,7 +7726,7 @@ sign_a_node(dns_db_t *db, dns_zone_t *zone, dns_name_t *name,
* Going from insecure to NSEC.
* Don't generate NSEC records for NSEC3 records.
*/
if (build_nsec && !seen_nsec3 && !seen_nsec && seen_rr) {
if (build_nsec && !seen.nsec3 && !seen.nsec && seen.rr) {
/*
* Build a NSEC record except at the origin.
*/
@ -7739,7 +7768,7 @@ sign_a_node(dns_db_t *db, dns_zone_t *zone, dns_name_t *name,
}
}
if (seen_ns && !seen_soa && rdataset.type != dns_rdatatype_ds &&
if (seen.ns && !seen.soa && rdataset.type != dns_rdatatype_ds &&
rdataset.type != dns_rdatatype_nsec)
{
goto next_rdataset;
@ -8437,8 +8466,7 @@ zone_nsec3chain(dns_zone_t *zone) {
unsigned int nkeys = 0;
uint32_t nodes;
bool unsecure = false;
bool seen_soa, seen_ns, seen_dname, seen_ds;
bool seen_nsec, seen_nsec3, seen_rr;
seen_t seen;
dns_rdatasetiter_t *iterator = NULL;
bool buildnsecchain;
bool updatensec = false;
@ -8606,45 +8634,27 @@ zone_nsec3chain(dns_zone_t *zone) {
/*
* Check to see if this is a bottom of zone node.
*/
result = dns_db_allrdatasets(db, node, version, 0, 0,
&iterator);
result = allrdatasets(db, node, version, &iterator, &seen);
if (result == ISC_R_NOTFOUND) {
/* Empty node? */
goto next_addnode;
}
CHECK(result);
seen_soa = seen_ns = seen_dname = seen_ds = seen_nsec = false;
for (result = dns_rdatasetiter_first(iterator);
result == ISC_R_SUCCESS;
result = dns_rdatasetiter_next(iterator))
{
dns_rdatasetiter_current(iterator, &rdataset);
INSIST(rdataset.type != dns_rdatatype_nsec3);
if (rdataset.type == dns_rdatatype_soa) {
seen_soa = true;
} else if (rdataset.type == dns_rdatatype_ns) {
seen_ns = true;
} else if (rdataset.type == dns_rdatatype_dname) {
seen_dname = true;
} else if (rdataset.type == dns_rdatatype_ds) {
seen_ds = true;
} else if (rdataset.type == dns_rdatatype_nsec) {
seen_nsec = true;
}
dns_rdataset_disassociate(&rdataset);
}
INSIST(!seen.nsec3);
dns_rdatasetiter_destroy(&iterator);
/*
* Is there a NSEC chain than needs to be cleaned up?
*/
if (seen_nsec) {
if (seen.nsec) {
nsec3chain->seen_nsec = true;
}
if (seen_ns && !seen_soa && !seen_ds) {
if (seen.ns && !seen.soa && !seen.ds) {
unsecure = true;
}
if ((seen_ns && !seen_soa) || seen_dname) {
if ((seen.ns && !seen.soa) || seen.dname) {
delegation = true;
}
@ -8803,7 +8813,7 @@ zone_nsec3chain(dns_zone_t *zone) {
if (first) {
dnssec_log(zone, ISC_LOG_DEBUG(3),
"zone_nsec3chain:buildnsecchain = %u\n",
"zone_nsec3chain:buildnsecchain = %u",
buildnsecchain);
}
@ -8868,42 +8878,19 @@ zone_nsec3chain(dns_zone_t *zone) {
/*
* Check to see if this is a bottom of zone node.
*/
result = dns_db_allrdatasets(db, node, version, 0, 0,
&iterator);
result = allrdatasets(db, node, version, &iterator, &seen);
if (result == ISC_R_NOTFOUND) {
/* Empty node? */
goto next_removenode;
}
CHECK(result);
seen_soa = seen_ns = seen_dname = seen_nsec3 = seen_nsec =
seen_rr = false;
for (result = dns_rdatasetiter_first(iterator);
result == ISC_R_SUCCESS;
result = dns_rdatasetiter_next(iterator))
{
dns_rdatasetiter_current(iterator, &rdataset);
if (rdataset.type == dns_rdatatype_soa) {
seen_soa = true;
} else if (rdataset.type == dns_rdatatype_ns) {
seen_ns = true;
} else if (rdataset.type == dns_rdatatype_dname) {
seen_dname = true;
} else if (rdataset.type == dns_rdatatype_nsec) {
seen_nsec = true;
} else if (rdataset.type == dns_rdatatype_nsec3) {
seen_nsec3 = true;
} else if (rdataset.type != dns_rdatatype_rrsig) {
seen_rr = true;
}
dns_rdataset_disassociate(&rdataset);
}
dns_rdatasetiter_destroy(&iterator);
if (!seen_rr || seen_nsec3 || seen_nsec) {
if (!seen.rr || seen.nsec3 || seen.nsec) {
goto next_removenode;
}
if ((seen_ns && !seen_soa) || seen_dname) {
if ((seen.ns && !seen.soa) || seen.dname) {
delegation = true;
}