From 24aa490a9bd33cc952ccfa1f6930f345188797d1 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Fri, 9 Jan 2026 12:19:42 +0100 Subject: [PATCH 1/2] Lower case the NSEC next owner name when signing When building the NSEC rdata, lower case the next owner name before storing it in the Next Domain Name Field. Note that this is not required according to RFC 6840, Section 5.1: When canonicalizing DNS names (for both ordering and signing), DNS names in the RDATA section of NSEC resource records are not converted to lowercase. DNS names in the RDATA section of RRSIG resource records are converted to lowercase. The guidance in the above paragraph differs from what has been published before but is consistent with current common practice. Item 3 of Section 6.2 of [RFC4034] says that names in both of these RR types should be converted to lowercase. The earlier [RFC3755] says that they should not. Since there is inconsistency in the documents over time, having uppercase next owner names in the NSEC records may cause validation failures if validators are not implementing RFC 6840. Also, RFC 4034 section 6.2 is not about how NSEC record content is created, but how RRset content is normalized in order to produce and validate RRSIG records for a given RRset. Since the next owner name of the NSEC record is about ordening, and the canonical DNS name order requires that uppercase US-ASCII letters must be treated as if they were lowercase US-ASCII letters, case is not meaningful for NSEC next owner names, as it cannot be compressed on the wire, so we may lowercase the next owner name in the NSEC rdata before signing, being more kind to validators. --- lib/dns/nsec.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/dns/nsec.c b/lib/dns/nsec.c index 74fde42ee6..d444374e8c 100644 --- a/lib/dns/nsec.c +++ b/lib/dns/nsec.c @@ -100,11 +100,18 @@ dns_nsec_buildrdata(dns_db_t *db, dns_dbversion_t *version, dns_dbnode_t *node, unsigned char *nsec_bits, *bm; unsigned int max_type; dns_rdatasetiter_t *rdsiter; + dns_fixedname_t fnextname; + dns_name_t *nextname; REQUIRE(target != NULL); + /* + * Downcase next owner name. + */ + nextname = dns_fixedname_initname(&fnextname); + RUNTIME_CHECK(dns_name_downcase(target, nextname) == ISC_R_SUCCESS); memset(buffer, 0, DNS_NSEC_BUFFERSIZE); - dns_name_toregion(target, &r); + dns_name_toregion(nextname, &r); memmove(buffer, r.base, r.length); r.base = buffer; /* From bcb65f52f264a3d9a48b0607abda403d1c3dcb8d Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Fri, 9 Jan 2026 11:32:43 +0100 Subject: [PATCH 2/2] Add kasp test zone with uppercase characters The test ensures that such zone is signed correctly. In addition, test that the next owner name field of the NSEC record is lowercased. --- bin/tests/system/isctest/kasp.py | 13 ++++--- bin/tests/system/kasp/ns3/named-fips.conf.j2 | 7 ++++ bin/tests/system/kasp/ns3/setup.sh | 6 +++- bin/tests/system/kasp/ns3/upper.kasp.db.in | 27 +++++++++++++++ bin/tests/system/kasp/tests_kasp.py | 36 ++++++++++++++++++++ 5 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 bin/tests/system/kasp/ns3/upper.kasp.db.in diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py index 255a5832cc..bf7b1d2573 100644 --- a/bin/tests/system/isctest/kasp.py +++ b/bin/tests/system/isctest/kasp.py @@ -952,6 +952,8 @@ def _check_signatures( zrrsig = False krrsig = not zrrsig + signer = fqdn.lower() + for key in keys: if key.external: continue @@ -963,7 +965,7 @@ def _check_signatures( alg = key.get_dnsalg() rtype = dns.rdatatype.to_text(covers) - expect = rf"IN RRSIG {rtype} {alg} (\d) (\d+) (\d+) (\d+) {key.tag} {fqdn}" + expect = rf"IN RRSIG {rtype} {alg} (\d) (\d+) (\d+) (\d+) {key.tag} {signer}" if zrrsig and zsigning: has_rrsig = False @@ -1572,17 +1574,18 @@ def keydir_to_keylist( """ if zone is None: zone = "" + zname = zone.lower() all_keys = [] if keydir is None: - regex = rf"(K{zone}\.\+.*\+.*)\.key" - for filename in glob.glob(f"K{zone}.+*+*.key"): + regex = rf"(K{zname}\.\+.*\+.*)\.key" + for filename in glob.glob(f"K{zname}.+*+*.key"): match = re.match(regex, filename) if match is not None: all_keys.append(Key(match.group(1))) else: - regex = rf"{keydir}/(K{zone}\.\+.*\+.*)\.key" - for filename in glob.glob(f"{keydir}/K{zone}.+*+*.key"): + regex = rf"{keydir}/(K{zname}\.\+.*\+.*)\.key" + for filename in glob.glob(f"{keydir}/K{zname}.+*+*.key"): match = re.match(regex, filename) if match is not None: all_keys.append(Key(match.group(1), keydir)) diff --git a/bin/tests/system/kasp/ns3/named-fips.conf.j2 b/bin/tests/system/kasp/ns3/named-fips.conf.j2 index 9255779175..19f3683e40 100644 --- a/bin/tests/system/kasp/ns3/named-fips.conf.j2 +++ b/bin/tests/system/kasp/ns3/named-fips.conf.j2 @@ -20,6 +20,13 @@ zone "default.kasp" { dnssec-policy "default"; }; +/* The UPPER case: a zone with uppercase characters. */ +zone "UPPER.KASP" { + type primary; + file "upper.kasp.db"; + dnssec-policy "default"; +}; + /* A zone with special characters. */ zone {% raw %}"i-am.\":\;?&[]\@!\$*+,|=\.\(\)special.kasp."{% endraw %} { type primary; diff --git a/bin/tests/system/kasp/ns3/setup.sh b/bin/tests/system/kasp/ns3/setup.sh index 756c0af19c..74b985dbcd 100644 --- a/bin/tests/system/kasp/ns3/setup.sh +++ b/bin/tests/system/kasp/ns3/setup.sh @@ -56,12 +56,16 @@ for zn in default dnssec-keygen some-keys legacy-keys pregenerated \ done # -# Setup special zone +# Setup special zones # zone="i-am.\":\;?&[]\@!\$*+,|=\.\(\)special.kasp." echo_i "setting up zone: $zone" cp template.db.in "i-am.special.kasp.db" +zone="UPPER.KASP." +echo_i "setting up zone: $zone" +cp upper.kasp.db.in "upper.kasp.db" + # # Set up RSASHA1 based zones # diff --git a/bin/tests/system/kasp/ns3/upper.kasp.db.in b/bin/tests/system/kasp/ns3/upper.kasp.db.in new file mode 100644 index 0000000000..13999f9dcf --- /dev/null +++ b/bin/tests/system/kasp/ns3/upper.kasp.db.in @@ -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 +UPPER.KASP. IN SOA MNAME1. . ( + 1 ; serial + 20 ; refresh (20 seconds) + 20 ; retry (20 seconds) + 1814400 ; expire (3 weeks) + 3600 ; minimum (1 hour) + ) + + NS ns3 +ns3 A 10.53.0.3 + +a A 10.0.0.1 +b A 10.0.0.2 +c A 10.0.0.3 + diff --git a/bin/tests/system/kasp/tests_kasp.py b/bin/tests/system/kasp/tests_kasp.py index f5a02152dc..60dd34c800 100644 --- a/bin/tests/system/kasp/tests_kasp.py +++ b/bin/tests/system/kasp/tests_kasp.py @@ -936,6 +936,42 @@ def test_kasp_default(ns3): check_all(ns3, zone, policy, keys, []) +def test_kasp_uppercase(ns3): + # check the zone with uppercase characters is loaded and signed. + isctest.log.info("check a zone with upper case characters is signed") + zone = "UPPER.KASP" + policy = "default" + + isctest.kasp.wait_keymgr_done(ns3, zone) + + # Key properties. + # DNSKEY, RRSIG (ksk), RRSIG (zsk) are published. DS needs to wait. + keyprops = [ + "csk 0 13 256 goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden", + ] + expected = isctest.kasp.policy_to_properties(ttl=3600, keys=keyprops) + keys = isctest.kasp.keydir_to_keylist(zone, "ns3") + isctest.kasp.check_dnssec_verify(ns3, zone) + isctest.kasp.check_keys(zone, keys, expected) + set_keytimes_default_policy(expected[0]) + isctest.kasp.check_keytimes(keys, expected) + check_all(ns3, zone, policy, keys, []) + + fqdn = f"{zone}." + query = isctest.query.create(fqdn, dns.rdatatype.NSEC) + response = isctest.query.tcp(query, ns3.ip) + assert response.rcode() == dns.rcode.NOERROR + + nsec = response.get_rrset( + response.answer, + dns.name.from_text(fqdn), + dns.rdataclass.IN, + dns.rdatatype.NSEC, + ) + nextname = nsec[0].next + assert str(nextname) == "a.upper.kasp." + + def test_kasp_dynamic(ns3): # Standard dynamic zone. isctest.log.info("check dynamic zone is updated and signed after update")