Merge kasp.Key functionality into zone.FileZoneKey

Make zone.FileZoneKey the single representation of a file-backed key
(typically generated by dnssec-keygen). Move the common key-related
functionality into zone.FileZoneKey, and extend that functionality in
kasp.Key to also add state and timing related operations on top. Remove
duplicate into_ta() function.

Note that is_ksk() is implemented differently for kasp.Key: with the
metadata file available, the KSK status is loaded from that file, as it
indicates the authoritative policy decision which makes the key a KSK.
In zone.FileZoneKey which doesn't work with the metadata file, the KSK
status if inferred from the DNSKEY SEP flag - the best information
available for that class.

Assisted-by: Claude:claude-opus-4-8
This commit is contained in:
Nicki Křížek 2026-06-16 12:34:48 +00:00
parent 9dc039f735
commit 35fc5ce8b4
2 changed files with 29 additions and 40 deletions

View file

@ -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:

View file

@ -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):