From 4cca73b037862e2bf2a1b6fbba3a93316fc86ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Tue, 10 Mar 2026 15:19:31 +0100 Subject: [PATCH] Create trust anchors from isctest.kasp.Key Add isctest.kasp.Key and the minimal methods which are required tp 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. (manually picked from 0bf20f8d and f6cb154b) --- bin/tests/system/_common/trusted.conf.j2 | 18 +++++ bin/tests/system/isctest/__init__.py | 1 + bin/tests/system/isctest/kasp.py | 91 ++++++++++++++++++++++++ bin/tests/system/isctest/template.py | 8 +++ 4 files changed, 118 insertions(+) create mode 100644 bin/tests/system/_common/trusted.conf.j2 create mode 100644 bin/tests/system/isctest/kasp.py 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/__init__.py b/bin/tests/system/isctest/__init__.py index 4af893202b..412d2dcc9b 100644 --- a/bin/tests/system/isctest/__init__.py +++ b/bin/tests/system/isctest/__init__.py @@ -13,6 +13,7 @@ from . import check from . import instance from . import hypothesis from . import query +from . import kasp from . import run from . import template from . import log diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py new file mode 100644 index 0000000000..4a1d381919 --- /dev/null +++ b/bin/tests/system/isctest/kasp.py @@ -0,0 +1,91 @@ +# 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. + +from functools import total_ordering +from pathlib import Path + +import dns.dnssec +import dns.exception +import dns.message +import dns.name +import dns.rcode +import dns.rdataclass +import dns.rdatatype +import dns.rrset +import dns.tsig +import dns.zone +import dns.zonefile + +from isctest.template import TrustAnchor + +DEFAULT_TTL = 300 + + +@total_ordering +class Key: + """ + Represent a key from a keyfile. + + This object keeps track of its origin (keydir + name), can be used to + retrieve metadata from the underlying files and supports convenience + operations for KASP tests. + """ + + def __init__(self, name: str, keydir: str | Path | None = None): + self.name = name + if keydir is None: + self.keydir = Path() + else: + self.keydir = Path(keydir) + self.path = str(self.keydir / name) + self.privatefile = f"{self.path}.private" + self.keyfile = f"{self.path}.key" + self.statefile = f"{self.path}.state" + self.tag = int(self.name[-5:]) + self.external = False + + @property + def dnskey(self) -> dns.rrset.RRset: + with open(self.keyfile, "r", encoding="utf-8") as file: + 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 into_ta(self, ta_type: str, dsdigest=dns.dnssec.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 __lt__(self, other: "Key"): + return self.name < other.name + + def __eq__(self, other: object): + return isinstance(other, Key) and self.path == other.path + + def __repr__(self): + return self.path diff --git a/bin/tests/system/isctest/template.py b/bin/tests/system/isctest/template.py index 3b175b9709..5555589460 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 import os from pathlib import Path from typing import Any, Dict, Optional, Union @@ -98,3 +99,10 @@ class TemplateEngine: ] for template in templates: self.render(template[:-3], data) + + +@dataclass +class TrustAnchor: + domain: str + type: str + contents: str