From 710bcd4ef742581e08378ea35677eaa5f58f80c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Fri, 24 Oct 2025 16:47:59 +0200 Subject: [PATCH 1/3] Parse DNSKEY into a dnspython type in isctest.kasp.Key.dnskey Previously, a DNSKEY string from keyfile was returned. This made the function brittle for further processing, as the string would have to be split up, concatenated, and TTL could be missing, making string indices context-dependent. Parse the DNSKEY rrset into a proper dnspython object and return it. This makes the output more predictable and reliable, as all the neccessary parsing is done by dnspython. (cherry picked from commit 0bf20f8d68705d2b4fa52a0c77a28ec25ab277af) --- bin/tests/system/isctest/kasp.py | 24 +++++++++++++++---- .../tests_rollover_multisigner.py | 12 ++++------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py index 3728e1e3d2..e41def525e 100644 --- a/bin/tests/system/isctest/kasp.py +++ b/bin/tests/system/isctest/kasp.py @@ -20,8 +20,12 @@ import time from typing import Dict, List, Optional, Tuple, Union import dns +import dns.rdatatype +import dns.rrset import dns.tsig +import pytest + import isctest.log import isctest.query import isctest.util @@ -435,12 +439,22 @@ class Key: return int(line.split()[1]) return 0 - def dnskey(self): + @property + def dnskey(self) -> dns.rrset.RRset: + pytest.importorskip("dns", minversion="2.2.0") # dns.zonefile.read_rrsets with open(self.keyfile, "r", encoding="utf-8") as file: - for line in file: - if "DNSKEY" in line: - return line.strip() - return "undefined" + rrsets = dns.zonefile.read_rrsets( + file.read(), + rdclass=None, # read rdclass from the file + default_ttl=DEFAULT_TTL, # use this TTL if not present + ) + assert len(rrsets) == 1, f"{self.keyfile} has multiple RRsets" + dnskey_rr = rrsets[0] + assert len(dnskey_rr) == 1, f"{self.keyfile} has multiple RRs" + assert ( + dnskey_rr.rdtype == dns.rdatatype.DNSKEY + ), f"DNSKEY not found in {self.keyfile}" + return dnskey_rr def is_ksk(self) -> bool: return self.get_metadata("KSK") == "yes" diff --git a/bin/tests/system/rollover-multisigner/tests_rollover_multisigner.py b/bin/tests/system/rollover-multisigner/tests_rollover_multisigner.py index 6b5a624403..7aad2d98cd 100644 --- a/bin/tests/system/rollover-multisigner/tests_rollover_multisigner.py +++ b/bin/tests/system/rollover-multisigner/tests_rollover_multisigner.py @@ -102,11 +102,10 @@ def test_rollover_multisigner(ns3, alg, size): expected2[0].legacy = True # noqa expected = expected + expected2 - dnskey = newkeys[0].dnskey().split() - rdata = " ".join(dnskey[4:]) + dnskey = newkeys[0].dnskey update_msg = dns.update.UpdateMessage(zone) - update_msg.add(f"{dnskey[0]}", 3600, "DNSKEY", rdata) + update_msg.add(dnskey.name, dnskey.ttl, dnskey[0]) ns3.nsupdate(update_msg) isctest.kasp.check_dnssec_verify(ns3, zone) @@ -118,11 +117,10 @@ def test_rollover_multisigner(ns3, alg, size): isctest.kasp.check_subdomain(ns3, zone, ksks, zsks) # Remove ZSKs from the other providers for zone. - dnskey2 = extkeys[0].dnskey().split() - rdata2 = " ".join(dnskey2[4:]) + dnskey2 = extkeys[0].dnskey update_msg = dns.update.UpdateMessage(zone) - update_msg.delete(f"{dnskey[0]}", "DNSKEY", rdata) - update_msg.delete(f"{dnskey2[0]}", "DNSKEY", rdata2) + update_msg.delete(dnskey.name, dnskey[0]) + update_msg.delete(dnskey2.name, dnskey2[0]) ns3.nsupdate(update_msg) isctest.kasp.check_dnssec_verify(ns3, zone) From e08aa3c1e9f8ab412c5615b62bdd39ace7b9bc96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Mon, 3 Nov 2025 14:59:00 +0100 Subject: [PATCH 2/3] Add a template for TA and generate it from isctest.kasp.Key Add isctest.kasp.Key.into_ta() method which convert the key into DS / DNSKEY trust anchor for BIND config. Add a shared template trusted.conf.j2 which can be linked to in tests to create the trust anchor configuration from trust anchor data returned from bootstrap() function. This is basically a python replacement for the keyfile_to_static_ds (and friends) from the conf.sh shell framework. (cherry picked from commit f6cb154b658f8ab74511bd001671303876586a24) --- bin/tests/system/_common/trusted.conf.j2 | 18 ++++++++++++++++++ bin/tests/system/isctest/kasp.py | 16 ++++++++++++++++ bin/tests/system/isctest/template.py | 8 ++++++++ 3 files changed, 42 insertions(+) create mode 100644 bin/tests/system/_common/trusted.conf.j2 diff --git a/bin/tests/system/_common/trusted.conf.j2 b/bin/tests/system/_common/trusted.conf.j2 new file mode 100644 index 0000000000..fef3a774e7 --- /dev/null +++ b/bin/tests/system/_common/trusted.conf.j2 @@ -0,0 +1,18 @@ +/* + * 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. + */ + +trust-anchors { +{% for ta in trust_anchors %} + "@ta.domain@" @ta.type@ @ta.contents@; +{% endfor %} +}; diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py index e41def525e..e8684883a5 100644 --- a/bin/tests/system/isctest/kasp.py +++ b/bin/tests/system/isctest/kasp.py @@ -20,6 +20,8 @@ import time from typing import Dict, List, Optional, Tuple, Union import dns +import dns.dnssec +from dns.dnssectypes import DSDigest import dns.rdatatype import dns.rrset import dns.tsig @@ -30,6 +32,7 @@ import isctest.log import isctest.query import isctest.util from isctest.instance import NamedInstance +from isctest.template import TrustAnchor from isctest.vars.algorithms import Algorithm, ALL_ALGORITHMS_BY_NUM DEFAULT_TTL = 300 @@ -456,6 +459,19 @@ class Key: ), f"DNSKEY not found in {self.keyfile}" return dnskey_rr + def into_ta(self, ta_type: str, dsdigest=DSDigest.SHA256) -> TrustAnchor: + dnskey = self.dnskey + if ta_type in ["static-ds", "initial-ds"]: + ds = dns.dnssec.make_ds(dnskey.name, dnskey[0], dsdigest) + parts = str(ds).split() + contents = " ".join(parts[:3]) + f' "{parts[3]}"' + elif ta_type in ["static-key", "initial-key"]: + parts = str(dnskey).split() + contents = " ".join(parts[4:7]) + f' "{"".join(parts[7:])}"' + else: + raise ValueError(f"invalid trust anchor type: {ta_type}") + return TrustAnchor(str(dnskey.name), ta_type, contents) + def is_ksk(self) -> bool: return self.get_metadata("KSK") == "yes" diff --git a/bin/tests/system/isctest/template.py b/bin/tests/system/isctest/template.py index 7ac7c8b4f0..4e27a219d1 100644 --- a/bin/tests/system/isctest/template.py +++ b/bin/tests/system/isctest/template.py @@ -11,6 +11,7 @@ # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. +from dataclasses import dataclass from pathlib import Path from typing import Any, Dict, Optional, Union @@ -79,3 +80,10 @@ class TemplateEngine: ] for template in templates: self.render(template[:-3], data) + + +@dataclass +class TrustAnchor: + domain: str + type: str + contents: str From bb20762627893486e69f0a1543757185d05ad076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Thu, 27 Nov 2025 13:16:26 +0100 Subject: [PATCH 3/3] Mock DSDigest for compatibility with older dnspython DSDigest class isn't available prior to dnspython 2.0.0 and prior to Add an isctest.compat.DSDigest compatibility hack to support those versions. (cherry picked from commit 2f2be20547f772459466c6695098e1649a7e8f82) --- bin/tests/system/isctest/compat.py | 14 ++++++++++++++ bin/tests/system/isctest/kasp.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/bin/tests/system/isctest/compat.py b/bin/tests/system/isctest/compat.py index 3dc5810745..ae6b157c0d 100644 --- a/bin/tests/system/isctest/compat.py +++ b/bin/tests/system/isctest/compat.py @@ -54,3 +54,17 @@ else: class EDEOption: def __new__(cls, *args, **kwargs): raise RuntimeError("Using EDEOption requires dnspython>=2.2.0") + + +# pylint: disable=unused-import +try: + from dns.dnssec import DSDigest +except ImportError: # dnspython<2.0.0 + import enum + + class DSDigest(enum.IntEnum): # type: ignore + """DNSSEC Delgation Signer Digest Algorithm""" + + SHA1 = 1 + SHA256 = 2 + SHA384 = 4 diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py index e8684883a5..beab977885 100644 --- a/bin/tests/system/isctest/kasp.py +++ b/bin/tests/system/isctest/kasp.py @@ -21,7 +21,6 @@ from typing import Dict, List, Optional, Tuple, Union import dns import dns.dnssec -from dns.dnssectypes import DSDigest import dns.rdatatype import dns.rrset import dns.tsig @@ -31,6 +30,7 @@ import pytest import isctest.log import isctest.query import isctest.util +from isctest.compat import DSDigest from isctest.instance import NamedInstance from isctest.template import TrustAnchor from isctest.vars.algorithms import Algorithm, ALL_ALGORITHMS_BY_NUM