Commit 3f043b7d8c for bind
commit 3f043b7d8cbb11780edc833622eb20e36b2613d1
Author: Nicki Křížek <nicki@isc.org>
Date: Fri Oct 2 10:03:27 2026 +0000
Validate RRSIGs with a key to check that the key made them
To decide whether a key signed an RRset, check_signatures() looked for
an RRSIG carrying the key's algorithm, key tag, and signer name. That
does not identify the key: distinct keys may share a key tag, e.g. an
offline KSK and a ZSK generated in different key directories, and an
RRSIG made by one of them then counts as made by the other as well.
Validate the RRSIG over the RRset with the DNSKEY of the given key
instead; only a successful validation shows that the key made it.
Assisted-by: Claude:claude-opus-5-5
diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py
index 8d85b6f0bd..83b7ba5064 100644
--- a/bin/tests/system/isctest/kasp.py
+++ b/bin/tests/system/isctest/kasp.py
@@ -19,6 +19,7 @@ import glob
import os
import re
+import dns.dnssec
import dns.exception
import dns.message
import dns.name
@@ -922,10 +923,24 @@ def check_dnssecstatus(server, zone, keys, policy=None, view=None, verbose=False
assert f"{key.role()} {key.tag}" in response.out
+def _signed_by(rrset, rrsig, dnskey) -> bool:
+ """
+ Check that 'rrsig' over 'rrset' was made with the key in 'dnskey'. The key
+ tag alone cannot tell, as tags of distinct keys may collide.
+ """
+ try:
+ dns.dnssec.validate_rrsig(
+ rrset, rrsig, {dnskey.name: dnskey}, now=rrsig.inception
+ )
+ except dns.dnssec.ValidationFailure:
+ return False
+ return True
+
+
def _check_signatures(
+ rrset,
signatures,
covers,
- fqdn,
keys,
offline_ksk=False,
zsk_missing=False,
@@ -935,9 +950,6 @@ def _check_signatures(
zrrsig = True
if covers in [dns.rdatatype.DNSKEY, dns.rdatatype.CDNSKEY, dns.rdatatype.CDS]:
zrrsig = False
- krrsig = not zrrsig
-
- signer = fqdn.lower()
for key in keys:
if key.external:
@@ -946,70 +958,53 @@ def _check_signatures(
ksigning, zsigning = key.get_signing_state(
offline_ksk=offline_ksk, zsk_missing=zsk_missing, smooth=smooth
)
+ signing = zsigning if zrrsig else ksigning
- alg = key.algorithm.number
- rtype = dns.rdatatype.to_text(covers)
-
- expect = rf"IN RRSIG {rtype} {alg} (\d) (\d+) (\d+) (\d+) {key.tag} {signer}"
-
- if zrrsig and zsigning:
- has_rrsig = False
- for rrsig in signatures:
- if re.search(expect, rrsig) is not None:
- has_rrsig = True
- break
- assert has_rrsig, f"Expected signature but not found: {expect}"
- numsigs += 1
-
- if zrrsig and not zsigning:
- for rrsig in signatures:
- assert re.search(expect, rrsig) is None
+ dnskey = key.dnskey
+ has_rrsig = rrset is not None and any(
+ _signed_by(rrset, rrsig, dnskey) for rrsig in signatures
+ )
- if krrsig and ksigning:
- has_rrsig = False
- for rrsig in signatures:
- if re.search(expect, rrsig) is not None:
- has_rrsig = True
- break
- assert has_rrsig, f"Expected signature but not found: {expect}"
+ if signing:
+ assert has_rrsig, f"Expected signature by {key} not found"
numsigs += 1
-
- if krrsig and not ksigning:
- for rrsig in signatures:
- assert re.search(expect, rrsig) is None
+ else:
+ assert not has_rrsig, f"Unexpected signature by {key} found"
return numsigs
def check_signatures(
- rrset, covers, fqdn, ksks, zsks, offline_ksk=False, zsk_missing=False, smooth=False
+ rrsets,
+ rrsigs,
+ covers,
+ ksks,
+ zsks,
+ offline_ksk=False,
+ zsk_missing=False,
+ smooth=False,
):
- # Check if signatures with covering type are signed with the right keys.
- # The right keys are the ones that expect a signature and have the
- # correct role.
- numsigs = 0
-
- signatures = []
- for rr in rrset:
- for rdata in rr:
- rdclass = dns.rdataclass.to_text(rr.rdclass)
- rdtype = dns.rdatatype.to_text(rr.rdtype)
- rrsig = f"{rr.name} {rr.ttl} {rdclass} {rdtype} {rdata}"
- signatures.append(rrsig)
+ # Check if the signatures over the 'covers' RRset in 'rrsets' are signed
+ # with the right keys. The right keys are the ones that expect a signature
+ # and have the correct role.
+ assert len(rrsets) <= 1
+ rrset = rrsets[0] if rrsets else None
+ signatures = [rrsig for rr in rrsigs for rrsig in rr]
+ numsigs = 0
numsigs += _check_signatures(
+ rrset,
signatures,
covers,
- fqdn,
ksks,
offline_ksk=offline_ksk,
zsk_missing=zsk_missing,
smooth=smooth,
)
numsigs += _check_signatures(
+ rrset,
signatures,
covers,
- fqdn,
zsks,
offline_ksk=offline_ksk,
zsk_missing=zsk_missing,
@@ -1206,7 +1201,7 @@ def check_apex(
dnskeys, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.DNSKEY, tsig=tsig)
check_dnskeys(dnskeys, ksks, zsks, manual_mode=manual_mode)
check_signatures(
- rrsigs, dns.rdatatype.DNSKEY, fqdn, ksks, zsks, offline_ksk=offline_ksk
+ dnskeys, rrsigs, dns.rdatatype.DNSKEY, ksks, zsks, offline_ksk=offline_ksk
)
# test soa query
@@ -1214,9 +1209,9 @@ def check_apex(
assert len(soa) == 1
assert f"{zone}. {DEFAULT_TTL} IN SOA" in soa[0].to_text()
check_signatures(
+ soa,
rrsigs,
dns.rdatatype.SOA,
- fqdn,
ksks,
zsks,
offline_ksk=offline_ksk,
@@ -1237,17 +1232,22 @@ def check_apex(
if len(cdnskeys) > 0:
assert len(rrsigs) > 0
check_signatures(
- rrsigs, dns.rdatatype.CDNSKEY, fqdn, ksks, zsks, offline_ksk=offline_ksk
+ cdnskeys,
+ rrsigs,
+ dns.rdatatype.CDNSKEY,
+ ksks,
+ zsks,
+ offline_ksk=offline_ksk,
)
# test cds query
- cds, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.CDS, tsig=tsig)
+ cdsrrsets, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.CDS, tsig=tsig)
if cds_delete:
- check_cdsdelete(cds, "0 0 0 00")
+ check_cdsdelete(cdsrrsets, "0 0 0 00")
else:
cdsrrs = []
- for rr in cds:
+ for rr in cdsrrsets:
for rdata in rr:
rdclass = dns.rdataclass.to_text(rr.rdclass)
rdtype = dns.rdatatype.to_text(rr.rdtype)
@@ -1262,10 +1262,15 @@ def check_apex(
else:
check_cds_prohibit(cdsrrs, ksks, alg)
- if len(cds) > 0:
+ if len(cdsrrsets) > 0:
assert len(rrsigs) > 0
check_signatures(
- rrsigs, dns.rdatatype.CDS, fqdn, ksks, zsks, offline_ksk=offline_ksk
+ cdsrrsets,
+ rrsigs,
+ dns.rdatatype.CDS,
+ ksks,
+ zsks,
+ offline_ksk=offline_ksk,
)
assert numcds == len(cdsrrs)
@@ -1275,13 +1280,13 @@ def check_subdomain(
server, zone, ksks, zsks, offline_ksk=False, smooth=False, tsig=None
):
# Test an RRset below the apex and verify it is signed correctly.
- fqdn = f"{zone}."
qname = f"a.{zone}."
qtype = dns.rdatatype.A
response = _query(server, qname, qtype, tsig=tsig)
assert response.rcode() == dns.rcode.NOERROR
match = f"{qname} {DEFAULT_TTL} IN A 10.0.0.1"
+ rrs = []
rrsigs = []
for rrset in response.answer:
if rrset.match(
@@ -1290,9 +1295,10 @@ def check_subdomain(
rrsigs.append(rrset)
else:
assert match in rrset.to_text()
+ rrs.append(rrset)
check_signatures(
- rrsigs, qtype, fqdn, ksks, zsks, offline_ksk=offline_ksk, smooth=smooth
+ rrs, rrsigs, qtype, ksks, zsks, offline_ksk=offline_ksk, smooth=smooth
)
@@ -1393,7 +1399,7 @@ def check_rollover_step(server, config, policy, step):
return expected
-def verify_update_is_signed(server, fqdn, qname, qtype, rdata, ksks, zsks, tsig=None):
+def verify_update_is_signed(server, qname, qtype, rdata, ksks, zsks, tsig=None):
"""
Test an RRset below the apex and verify it is updated and signed correctly.
"""
@@ -1404,6 +1410,7 @@ def verify_update_is_signed(server, fqdn, qname, qtype, rdata, ksks, zsks, tsig=
rrtype = dns.rdatatype.to_text(qtype)
match = f"{qname} {DEFAULT_TTL} IN {rrtype} {rdata}"
+ rrs = []
rrsigs = []
for rrset in response.answer:
if rrset.match(
@@ -1412,12 +1419,14 @@ def verify_update_is_signed(server, fqdn, qname, qtype, rdata, ksks, zsks, tsig=
rrsigs.append(rrset)
elif not match in rrset.to_text():
return False
+ else:
+ rrs.append(rrset)
if len(rrsigs) == 0:
return False
# Zone is updated, ready to verify the signatures.
- check_signatures(rrsigs, qtype, fqdn, ksks, zsks)
+ check_signatures(rrs, rrsigs, qtype, ksks, zsks)
return True
@@ -1435,6 +1444,7 @@ def verify_rrsig_is_refreshed(
rrtype = dns.rdatatype.to_text(qtype)
match = f"{qname}. {DEFAULT_TTL} IN {rrtype}"
+ rrs = []
rrsigs = []
for rrset in response.answer:
if rrset.match(
@@ -1443,6 +1453,8 @@ def verify_rrsig_is_refreshed(
rrsigs.append(rrset)
elif not match in rrset.to_text():
return False
+ else:
+ rrs.append(rrset)
if len(rrsigs) == 0:
return False
@@ -1468,7 +1480,7 @@ def verify_rrsig_is_refreshed(
return False
# Zone is updated, ready to verify the signatures.
- check_signatures(rrsigs, qtype, fqdn, ksks, zsks)
+ check_signatures(rrs, rrsigs, qtype, ksks, zsks)
return True
@@ -1483,6 +1495,7 @@ def verify_rrsig_is_reused(server, fqdn, zonefile, qname, qtype, ksks, zsks, tsi
rrtype = dns.rdatatype.to_text(qtype)
match = f"{qname}. {DEFAULT_TTL} IN {rrtype}"
+ rrs = []
rrsigs = []
for rrset in response.answer:
if rrset.match(
@@ -1491,6 +1504,7 @@ def verify_rrsig_is_reused(server, fqdn, zonefile, qname, qtype, ksks, zsks, tsi
rrsigs.append(rrset)
else:
assert match in rrset.to_text()
+ rrs.append(rrset)
tmp_zonefile = f"{zonefile}.tmp"
isctest.run.cmd(
@@ -1511,7 +1525,7 @@ def verify_rrsig_is_reused(server, fqdn, zonefile, qname, qtype, ksks, zsks, tsi
for rrsig in rrsigs:
assert isctest.util.zone_contains(zone, rrsig)
- check_signatures(rrsigs, qtype, fqdn, ksks, zsks)
+ check_signatures(rrs, rrsigs, qtype, ksks, zsks)
def next_key_event_equals(server, zone, next_event):
diff --git a/bin/tests/system/kasp/tests_kasp.py b/bin/tests/system/kasp/tests_kasp.py
index 42f45371ad..a789a74623 100644
--- a/bin/tests/system/kasp/tests_kasp.py
+++ b/bin/tests/system/kasp/tests_kasp.py
@@ -233,7 +233,7 @@ def cb_ixfr_is_signed(expected_updates, params, ksks=None, zsks=None):
qtype = dns.rdatatype.from_text(parts[1])
rdata = parts[2]
return isctest.kasp.verify_update_is_signed(
- servers["ns3"], zone, qname, qtype, rdata, ksks, zsks
+ servers["ns3"], qname, qtype, rdata, ksks, zsks
)
for update in expected_updates:
@@ -825,7 +825,6 @@ def test_kasp_inherit_view(number, dynamic, inline_signing, txt_rdata, ns4):
assert f"dynamic: {dynamic}" in response.out
assert f"inline signing: {inline_signing}" in response.out
# check subdomain
- fqdn = f"{zone}."
qname = f"view.{zone}."
qtype = dns.rdatatype.TXT
rdata = txt_rdata
@@ -840,6 +839,7 @@ def test_kasp_inherit_view(number, dynamic, inline_signing, txt_rdata, ns4):
response = None
assert response.rcode() == dns.rcode.NOERROR
match = f'{qname} 300 IN TXT "{rdata}"'
+ rrs = []
rrsigs = []
for rrset in response.answer:
if rrset.match(
@@ -848,8 +848,9 @@ def test_kasp_inherit_view(number, dynamic, inline_signing, txt_rdata, ns4):
rrsigs.append(rrset)
else:
assert match in rrset.to_text()
+ rrs.append(rrset)
assert len(rrsigs) > 0
- isctest.kasp.check_signatures(rrsigs, qtype, fqdn, keys, [])
+ isctest.kasp.check_signatures(rrs, rrsigs, qtype, keys, [])
def test_kasp_default(ns3):
@@ -910,9 +911,7 @@ def test_kasp_default(ns3):
qname = parts[0]
qtype = dns.rdatatype.from_text(parts[1])
rdata = parts[2]
- return isctest.kasp.verify_update_is_signed(
- ns3, zone, qname, qtype, rdata, keys, []
- )
+ return isctest.kasp.verify_update_is_signed(ns3, qname, qtype, rdata, keys, [])
expected_updates = [f"a.{zone}. A 10.0.0.11", f"d.{zone}. A 10.0.0.44"]
for update in expected_updates:
@@ -1010,9 +1009,7 @@ def test_kasp_dynamic(ns3):
qname = parts[0]
qtype = dns.rdatatype.from_text(parts[1])
rdata = parts[2]
- return isctest.kasp.verify_update_is_signed(
- ns3, zone, qname, qtype, rdata, keys, []
- )
+ return isctest.kasp.verify_update_is_signed(ns3, qname, qtype, rdata, keys, [])
update_msg = dns.update.UpdateMessage(zone)
update_msg.delete(f"a.{zone}.", "A", "10.0.0.1")