diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py index b919753dd7..e86b9d6a81 100644 --- a/bin/tests/system/isctest/kasp.py +++ b/bin/tests/system/isctest/kasp.py @@ -20,22 +20,19 @@ import os import re import time -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.instance import NamedInstance from isctest.run import EnvCmd -from isctest.template import TrustAnchor from isctest.vars.algorithms import ALL_ALGORITHMS_BY_NUM, Algorithm +from isctest.zone import FileZoneKey import isctest.log import isctest.query @@ -380,26 +377,18 @@ class SettimeOptions: @total_ordering -class Key: +class Key(FileZoneKey): """ - Represent a key from a keyfile. + A FileZoneKey specialized with KASP timing and state-file operations. - 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. + Inherits the key-material accessors (dnskey, into_ta, ...) from FileZoneKey + and adds the metadata reads, signing-state derivation, and timing + convenience operations used by the KASP/rollover 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" + super().__init__(name, keydir) self.statefile = f"{self.path}.state" - self.tag = int(self.name[-5:]) self.external = False def get_timing( @@ -522,20 +511,9 @@ class Key: ), 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 is_ksk(self) -> bool: + # KASP role follows the .state KSK metadata, not the DNSKEY SEP flag: + # a CSK may be configured without SEP (see the csk-nosep test). return self.get_metadata("KSK") == "yes" def is_zsk(self) -> bool: diff --git a/bin/tests/system/isctest/zone.py b/bin/tests/system/isctest/zone.py index df65932541..02b93c8b8d 100644 --- a/bin/tests/system/isctest/zone.py +++ b/bin/tests/system/isctest/zone.py @@ -27,8 +27,8 @@ import dns.name import dns.rdataclass import dns.rdatatype import dns.rrset +import dns.zonefile -from .kasp import Key from .log import debug from .run import EnvCmd from .template import NS1, Nameserver, TemplateEngine, TrustAnchor @@ -50,7 +50,7 @@ class ZoneKey(ABC): Abstract base for a DNSSEC key attached to a Zone. Two concrete implementations exist: - FileZoneKey — wraps a dnssec-keygen-managed key file pair (kasp.Key). + FileZoneKey — reads a dnssec-keygen-managed key file pair. PythonZoneKey — holds a Python-native (private_key, dnskey_rdata) pair required for dnspython-based operations and signing. @@ -107,18 +107,28 @@ class FileZoneKey(ZoneKey): """ A ZoneKey backed by dnssec-keygen-managed key files. - Constructed by FileZoneKey.generate(); callers normally do not - instantiate this directly. The underlying kasp.Key is accessible via - .key for working with timing metadata, state files, etc. + Reads key material directly from the .key/.private file pair. Normally + constructed via FileZoneKey.generate() (or Zone.add_keys()); + isctest.kasp.Key subclasses it to add KASP timing/state operations. """ - def __init__(self, key: Key, zone: Zone) -> None: - self.key = key + def __init__( + self, + name: str, + keydir: str | Path | None = None, + zone: Zone | None = None, + ) -> None: + self.name = name + self.keydir = Path() if keydir is None else Path(keydir) + self.path = str(self.keydir / name) + self.privatefile = f"{self.path}.private" + self.keyfile = f"{self.path}.key" + self.tag = int(self.name[-5:]) self.zone = zone @property def dnskey(self) -> dns.rrset.RRset: - return self.key.dnskey + raise NotImplementedError # TODO will be re-implemented in the followup commit def write_dsset( self, @@ -134,6 +144,7 @@ class FileZoneKey(ZoneKey): PythonZoneKey KSKs (PythonZoneKey appends to the same file); Zone.copy_dssets enforces this. """ + assert self.zone is not None, "write_dsset requires a zone-attached key" src = Path(self.zone.ns.name) / f"dsset-{self.zone.name}." shutil.copy(src, Path(target_dir)) debug(f"{self.zone.name}: dsset copied to {target_dir}") @@ -160,7 +171,7 @@ class FileZoneKey(ZoneKey): "KEYGEN", f"-q -a {alg.number} -b {alg.bits} -K {KEYDIR} -L {DNSKEY_TTL}" ) key_name = keygen(f"{params} {zone.name}", cwd=zone.ns.name).out.strip() - return FileZoneKey(Key(key_name, keydir=keydir), zone=zone) + return FileZoneKey(key_name, keydir=keydir, zone=zone) class PythonZoneKey(ZoneKey):