Commit 6a4ba15807 for bind

commit 6a4ba15807934fd703bf8f26810506025fc83073
Author: Michał Kępień <michal@isc.org>
Date:   Fri Oct 30 14:39:01 2026 +0100

    Simplify dnssec_nsec3_delegation test

    Replace boilerplate code signing the zone used by the ans1 custom
    server with canonical bootstrap() code employing isctest.zone.Zone.

    Replace boilerplate code crafting DNSSEC responses in ans1/*.py with
    zone file data in ans1/zones/ signed in bootstrap(), reducing
    ans1/ans.py down to just the logic implementing the attack attempted by
    the test.

    Replace fixed IP addresses with constants exposed by the
    isctest.template module.

diff --git a/bin/tests/system/dnssec_nsec3/ans1/ans.py b/bin/tests/system/dnssec_nsec3/ans1/ans.py
index 4150f82ea8..f840b99c24 100644
--- a/bin/tests/system/dnssec_nsec3/ans1/ans.py
+++ b/bin/tests/system/dnssec_nsec3/ans1/ans.py
@@ -60,12 +60,8 @@ class AncestorWildcardNsec3Handler(ResponseHandler):


 def main() -> None:
-    keys = common.load_keys()
-    server = AsyncDnsServer(default_aa=True)
-    server.install_response_handlers(
-        AncestorWildcardNsec3Handler(),
-        f055.F055Handler(keys),
-    )
+    server = AsyncDnsServer()
+    server.install_response_handlers(AncestorWildcardNsec3Handler())
     server.run()


diff --git a/bin/tests/system/dnssec_nsec3/ans1/common.py b/bin/tests/system/dnssec_nsec3/ans1/common.py
deleted file mode 100644
index a9699dc091..0000000000
--- a/bin/tests/system/dnssec_nsec3/ans1/common.py
+++ /dev/null
@@ -1,116 +0,0 @@
-# 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 dataclasses import dataclass
-from pathlib import Path
-
-import json
-
-from cryptography.hazmat.primitives import serialization
-
-import dns.dnssec
-import dns.flags
-import dns.message
-import dns.name
-import dns.rcode
-import dns.rdata
-import dns.rdataclass
-import dns.rdatatype
-import dns.rrset
-
-from isctest.asyncserver import QueryContext
-
-TTL = 300
-
-
-@dataclass(frozen=True)
-class Key:
-    zone: dns.name.Name
-    private_key: object
-    dnskey: dns.rdata.Rdata
-    ds: dns.rdata.Rdata
-
-
-def name(text: str) -> dns.name.Name:
-    return dns.name.from_text(text)
-
-
-def load_keys() -> dict[str, Key]:
-    path = Path(".") / "keys.json"
-    keys = raw_keys = {}
-
-    try:
-        with path.open(encoding="utf-8") as keys_file:
-            raw_keys = json.load(keys_file)
-    except Exception:  # pylint: disable=broad-except
-        pass
-
-    for zone, raw_key in raw_keys.items():
-        private_key = serialization.load_pem_private_key(
-            raw_key["private_pem"].encode("ascii"),
-            password=None,
-        )
-        dnskey = dns.rdata.from_text(
-            dns.rdataclass.IN, dns.rdatatype.DNSKEY, raw_key["dnskey"]
-        )
-        ds = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DS, raw_key["ds"])
-        keys[zone] = Key(name(zone), private_key, dnskey, ds)
-
-    return keys
-
-
-def rrset(owner: str, rdtype: dns.rdatatype.RdataType, *rdatas: str) -> dns.rrset.RRset:
-    return dns.rrset.from_text(owner, TTL, dns.rdataclass.IN, rdtype, *rdatas)
-
-
-def rrset_from_rdata(owner: str, rdata: dns.rdata.Rdata) -> dns.rrset.RRset:
-    return dns.rrset.from_rdata(owner, TTL, rdata)
-
-
-def add_signed(
-    section: list[dns.rrset.RRset], covered: dns.rrset.RRset, signer: Key
-) -> None:
-    rrsig = dns.dnssec.sign(
-        covered,
-        signer.private_key,
-        signer.zone,
-        signer.dnskey,
-        lifetime=86400,
-        verify=True,
-    )
-    section.append(covered)
-    section.append(dns.rrset.from_rdata(covered.name, covered.ttl, rrsig))
-
-
-def soa_rrset(zone) -> dns.rrset.RRset:
-    return rrset(
-        zone,
-        dns.rdatatype.SOA,
-        f"ns.{zone} hostmaster.{zone} 1 3600 600 86400 300",
-    )
-
-
-def prepare_response(qctx: QueryContext) -> dns.message.Message:
-    qctx.prepare_new_response(with_zone_data=False)
-    qctx.response.flags |= dns.flags.AA
-    qctx.response.set_rcode(dns.rcode.NOERROR)
-    return qctx.response
-
-
-def nsec3_hash(owner: str) -> str:
-    return dns.dnssec.nsec3_hash(owner, salt=None, iterations=0, algorithm=1).upper()
-
-
-def nsec3_rrset(
-    zone: str, owner_hash: str, next_hash: str, flags: int, *types: str
-) -> dns.rrset.RRset:
-    rdata = f"1 {flags} 0 - {next_hash} {' '.join(types)}"
-    return rrset(f"{owner_hash}.{zone}", dns.rdatatype.NSEC3, rdata)
diff --git a/bin/tests/system/dnssec_nsec3/ans1/f055.py b/bin/tests/system/dnssec_nsec3/ans1/f055.py
deleted file mode 100644
index 3aff055ace..0000000000
--- a/bin/tests/system/dnssec_nsec3/ans1/f055.py
+++ /dev/null
@@ -1,224 +0,0 @@
-# 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 collections.abc import AsyncGenerator
-from dataclasses import dataclass
-from datetime import datetime, timedelta, timezone
-
-import base64
-
-import dns.dnssec
-import dns.message
-import dns.rcode
-import dns.rdata
-import dns.rdataclass
-import dns.rdatatype
-import dns.rrset
-
-from dnssec_nsec3.ans1.common import (
-    Key,
-    add_signed,
-    name,
-    nsec3_hash,
-    nsec3_rrset,
-    prepare_response,
-    rrset,
-    rrset_from_rdata,
-    soa_rrset,
-)
-from isctest.asyncserver import QueryContext, ResponseHandler
-from isctest.asyncserver.actions import DnsResponseSend
-from isctest.asyncserver.matchers import Domain
-
-TTL = 300
-F055_ZONE = "f055.test."
-SAFE = f"safe.{F055_ZONE}"
-CUT = f"cut.{F055_ZONE}"
-CONTROL = f"ctl.{F055_ZONE}"
-ATTACK = f"a.{CUT}"
-CONTROL_ATTACK = f"a.{CONTROL}"
-SAFE_A = "198.51.100.55"
-FORGED_A = "192.0.2.55"
-
-
-def garbage_rrsig(covered: dns.rrset.RRset, signer: Key) -> dns.rrset.RRset:
-    now = datetime.now(timezone.utc)
-    inception = (now - timedelta(hours=1)).strftime("%Y%m%d%H%M%S")
-    expiration = (now + timedelta(days=1)).strftime("%Y%m%d%H%M%S")
-    signature = base64.b64encode(bytes(64)).decode("ascii")
-    labels = len(covered.name.labels) - 1
-    text = (
-        f"{dns.rdatatype.to_text(covered.rdtype)} "
-        f"13 {labels} {covered.ttl} {expiration} {inception} "
-        f"{dns.dnssec.key_id(signer.dnskey)} {F055_ZONE} {signature}"
-    )
-    rdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.RRSIG, text)
-    return dns.rrset.from_rdata(covered.name, covered.ttl, rdata)
-
-
-def base32hex_add(hash_text: str, delta: int) -> str:
-    raw = bytearray(base64.b32hexdecode(hash_text.upper()))
-    value = int.from_bytes(raw, "big") + delta
-    value %= 1 << (8 * len(raw))
-    return base64.b32hexencode(value.to_bytes(len(raw), "big")).decode("ascii")
-
-
-def forged_cut_nsec3() -> dns.rrset.RRset:
-    cut_hash = nsec3_hash(CUT)
-    return nsec3_rrset(
-        F055_ZONE,
-        base32hex_add(cut_hash, -1),
-        base32hex_add(cut_hash, 1),
-        1,
-        "A",
-        "RRSIG",
-    )
-
-
-@dataclass(frozen=True)
-class ChainEntry:
-    owner: str
-    owner_hash: str
-    types: tuple[str, ...]
-
-
-class Nsec3Chain:
-    def __init__(self) -> None:
-        entries = [
-            (F055_ZONE, ("NS", "SOA", "RRSIG", "DNSKEY", "NSEC3PARAM")),
-            (SAFE, ("A", "RRSIG")),
-            (CUT, ("A", "RRSIG")),
-            (CONTROL, ("A", "RRSIG")),
-        ]
-        self.entries = sorted(
-            (ChainEntry(owner, nsec3_hash(owner), types) for owner, types in entries),
-            key=lambda entry: entry.owner_hash,
-        )
-        self.by_owner = {entry.owner: entry for entry in self.entries}
-
-    def rrset_for(self, owner: str) -> dns.rrset.RRset:
-        entry = self.by_owner[owner]
-        index = self.entries.index(entry)
-        next_hash = self.entries[(index + 1) % len(self.entries)].owner_hash
-        return nsec3_rrset(F055_ZONE, entry.owner_hash, next_hash, 0, *entry.types)
-
-    def cover_for(self, owner: str) -> dns.rrset.RRset:
-        owner_hash = nsec3_hash(owner)
-        for index, entry in enumerate(self.entries):
-            next_hash = self.entries[(index + 1) % len(self.entries)].owner_hash
-            if entry.owner_hash < next_hash:
-                if entry.owner_hash < owner_hash < next_hash:
-                    return self.rrset_for(entry.owner)
-            elif owner_hash > entry.owner_hash or owner_hash < next_hash:
-                return self.rrset_for(entry.owner)
-        return self.rrset_for(self.entries[-1].owner)
-
-
-def add_signed_nsec3(
-    section: list[dns.rrset.RRset],
-    chain: Nsec3Chain,
-    owner: str,
-    signer: Key,
-) -> None:
-    add_signed(section, chain.rrset_for(owner), signer)
-
-
-def add_nsec3_nxdomain(
-    response: dns.message.Message,
-    chain: Nsec3Chain,
-    closest: str,
-    qname: str,
-    signer: Key,
-) -> None:
-    response.set_rcode(dns.rcode.NXDOMAIN)
-    add_signed(response.authority, soa_rrset(F055_ZONE), signer)
-    add_signed_nsec3(response.authority, chain, closest, signer)
-    add_signed(response.authority, chain.cover_for(qname), signer)
-    add_signed(response.authority, chain.cover_for(f"*.{closest}"), signer)
-
-
-class F055Handler(ResponseHandler):
-    matcher = Domain(F055_ZONE)
-
-    def __init__(self, keys: dict[str, Key]) -> None:
-        super().__init__()
-        self.keys = keys
-
-        if F055_ZONE not in keys:
-            return
-
-        self.zone_key = keys[F055_ZONE]
-        self.chain = Nsec3Chain()
-        self.zone = name(F055_ZONE)
-        self.safe = name(SAFE)
-        self.cut = name(CUT)
-        self.control = name(CONTROL)
-        self.attack = name(ATTACK)
-        self.control_attack = name(CONTROL_ATTACK)
-
-    async def get_responses(
-        self, qctx: QueryContext
-    ) -> AsyncGenerator[DnsResponseSend, None]:
-        response = prepare_response(qctx)
-        qname = qctx.qname.to_text()
-
-        if qctx.qname == self.zone and qctx.qtype == dns.rdatatype.DNSKEY:
-            # Priming, zone DNSKEY
-            add_signed(
-                response.answer,
-                rrset_from_rdata(F055_ZONE, self.zone_key.dnskey),
-                self.zone_key,
-            )
-        elif qctx.qname == self.zone and qctx.qtype == dns.rdatatype.SOA:
-            # Priming, zone SOA
-            add_signed(response.answer, soa_rrset(F055_ZONE), self.zone_key)
-        elif qctx.qname == self.safe and qctx.qtype == dns.rdatatype.A:
-            # Safe, good answer
-            add_signed(
-                response.answer, rrset(SAFE, dns.rdatatype.A, SAFE_A), self.zone_key
-            )
-        elif qctx.qname == self.cut and qctx.qtype == dns.rdatatype.DS:
-            # Forged DS answer
-            forged = forged_cut_nsec3()
-            response.authority.append(forged)
-            response.authority.append(garbage_rrsig(forged, self.zone_key))
-            add_signed_nsec3(response.authority, self.chain, CUT, self.zone_key)
-            add_signed(response.authority, soa_rrset(F055_ZONE), self.zone_key)
-        elif qctx.qname == self.control and qctx.qtype == dns.rdatatype.DS:
-            # Good DS answer
-            add_signed_nsec3(response.authority, self.chain, CONTROL, self.zone_key)
-            add_signed(response.authority, soa_rrset(F055_ZONE), self.zone_key)
-        elif qctx.qname in {self.attack, self.control_attack}:
-            # Attack query for CUT/CONTROL
-            if qctx.qtype == dns.rdatatype.A:
-                response.answer.append(rrset(qname, dns.rdatatype.A, FORGED_A))
-            elif qctx.qtype == dns.rdatatype.DS:
-                closest = CUT if qctx.qname == self.attack else CONTROL
-                add_nsec3_nxdomain(
-                    response,
-                    self.chain,
-                    closest,
-                    qname,
-                    self.zone_key,
-                )
-            else:
-                response.authority.append(soa_rrset(F055_ZONE))
-        elif qctx.qname.is_subdomain(self.cut):
-            # NXDOMAIN the rest
-            add_nsec3_nxdomain(response, self.chain, CUT, qname, self.zone_key)
-        elif qctx.qname.is_subdomain(self.control):
-            # NXDOMAIN the rest
-            add_nsec3_nxdomain(response, self.chain, CONTROL, qname, self.zone_key)
-        else:
-            # NXDOMAIN the rest
-            add_nsec3_nxdomain(response, self.chain, F055_ZONE, qname, self.zone_key)
-
-        yield DnsResponseSend(response, authoritative=True)
diff --git a/bin/tests/system/dnssec_nsec3/ans3/ans.py b/bin/tests/system/dnssec_nsec3/ans3/ans.py
new file mode 100644
index 0000000000..5db54fd514
--- /dev/null
+++ b/bin/tests/system/dnssec_nsec3/ans3/ans.py
@@ -0,0 +1,100 @@
+# 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 collections.abc import AsyncGenerator
+
+import dns.name
+import dns.rcode
+import dns.rdatatype
+import dns.rrset
+
+from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler
+from isctest.asyncserver.actions import DnsResponseSend
+from isctest.asyncserver.handlers import StaticResponseHandler
+from isctest.asyncserver.matchers import Qname, Qtype
+
+from ..dnssec_nsec3_ans import minimal_covering_nsec3, rrset
+
+
+def a(owner: str | dns.name.Name, address: str = "192.0.2.55") -> dns.rrset.RRset:
+    return rrset(owner, dns.rdatatype.A, address)
+
+
+class ControlAHandler(StaticResponseHandler):
+    """
+    This handler imitates successfully spoofing an unsigned response to an
+    "a.ctl.f055.test/A" query sent to a server authoritative for the
+    "f055.test." domain.  The subsequent "ctl.f055.test/DS" query sent by the
+    resolver is responded to with a properly signed NODATA response prepared
+    from zone data.  These two responses combined demonstrate that named
+    refuses to treat an unsigned answer to the A query as legitimate when the
+    DS non-existence proof is properly signed.
+    """
+
+    matcher = Qname("a.ctl.f055.test.") & Qtype(dns.rdatatype.A)
+    answer = [a(matcher.of(Qname).qnames[0])]
+
+
+class AttackAHandler(StaticResponseHandler):
+    """
+    This handler imitates successfully spoofing an unsigned response to an
+    "a.cut.f055.test/A" query sent to a server authoritative for the
+    "f055.test." domain.  The subsequent "cut.f055.test/DS" query sent by the
+    resolver is responded to by AttackDsHandler, which makes a vulnerable
+    resolver treat an unsigned answer to the A query as a legitimate insecure
+    response.
+    """
+
+    matcher = Qname("a.cut.f055.test.") & Qtype(dns.rdatatype.A)
+    answer = [a(matcher.of(Qname).qnames[0])]
+
+
+class AttackDsHandler(ResponseHandler):
+    """
+    This handler imitates successfully spoofing a response to a
+    "cut.f055.test/DS" query sent to a server authoritative for the
+    "f055.test." domain.  The spoofed response contains a properly signed
+    zone-based NODATA response (which can be replayed by an attacker) with an
+    extra forged NSEC3 record also proving that the DS record does not exist,
+    but accompanied by a bogus RRSIG record.  A vulnerable resolver treats such
+    a broken non-existence proof as sufficient for returning an unsigned answer
+    to the A query to the client.
+    """
+
+    matcher = Qname("cut.f055.test.") & Qtype(dns.rdatatype.DS)
+
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[DnsResponseSend, None]:
+        assert qctx.zone
+        assert qctx.zone.origin
+        forged_nsec3_covering_rrset = minimal_covering_nsec3(
+            qctx.qname, qctx.zone.origin, opt_out=True
+        )
+        forged_nsec3_covering_rrsig = qctx.sign(forged_nsec3_covering_rrset, bogus=True)
+        qctx.response.authority.append(forged_nsec3_covering_rrset)
+        qctx.response.authority.append(forged_nsec3_covering_rrsig)
+
+        yield DnsResponseSend(qctx.response)
+
+
+def main() -> None:
+    server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR)
+    server.install_response_handlers(
+        ControlAHandler(),
+        AttackAHandler(),
+        AttackDsHandler(),
+    )
+    server.run()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/bin/tests/system/dnssec_nsec3/ans3/zones/f055.test.db.j2.manual b/bin/tests/system/dnssec_nsec3/ans3/zones/f055.test.db.j2.manual
new file mode 100644
index 0000000000..e54de017ab
--- /dev/null
+++ b/bin/tests/system/dnssec_nsec3/ans3/zones/f055.test.db.j2.manual
@@ -0,0 +1,5 @@
+{% include "_common/zones/template.db.j2.manual" %}
+
+safe	A	198.51.100.55
+ctl	A	127.0.0.1
+cut	A	127.0.0.1
diff --git a/bin/tests/system/dnssec_nsec3/dnssec_nsec3_ans.py b/bin/tests/system/dnssec_nsec3/dnssec_nsec3_ans.py
index ff07b20e0e..71703c7eef 100644
--- a/bin/tests/system/dnssec_nsec3/dnssec_nsec3_ans.py
+++ b/bin/tests/system/dnssec_nsec3/dnssec_nsec3_ans.py
@@ -41,7 +41,7 @@ def rrset(


 def minimal_covering_nsec3(
-    name: dns.name.Name, origin: dns.name.Name
+    name: dns.name.Name, origin: dns.name.Name, opt_out: bool = False
 ) -> dns.rrset.RRset:
     nsec3_hash = dns.dnssec.nsec3_hash(name, None, 0, 1)
     nsec3_hash_just_before = base32hex_delta(nsec3_hash, -1)
@@ -50,5 +50,5 @@ def minimal_covering_nsec3(
     return rrset(
         nsec3_owner,
         dns.rdatatype.NSEC3,
-        f"1 0 0 - {nsec3_hash_just_after} TXT RRSIG",
+        f'1 {"1" if opt_out else "0"} 0 - {nsec3_hash_just_after} TXT RRSIG',
     )
diff --git a/bin/tests/system/dnssec_nsec3/ns2/named.conf.j2 b/bin/tests/system/dnssec_nsec3/ns2/named.conf.j2
index 9c3a3e40ca..2d3b399ae7 100644
--- a/bin/tests/system/dnssec_nsec3/ns2/named.conf.j2
+++ b/bin/tests/system/dnssec_nsec3/ns2/named.conf.j2
@@ -18,13 +18,7 @@ zone "f025.test" {

 zone "f055.test" {
 	type static-stub;
-	server-addresses { 10.53.0.1; };
+	server-addresses { 10.53.0.3; };
 };

-{% if ZONE_DNSKEY is defined %}
-trust-anchors {
-	f055.test. static-key 257 3 13 "@ZONE_DNSKEY@";
-};
-{% else %}
 {% include "_common/trusted.conf.j2" %}
-{% endif %}
diff --git a/bin/tests/system/dnssec_nsec3/tests_dnssec_nsec3_delegation.py b/bin/tests/system/dnssec_nsec3/tests_dnssec_nsec3_delegation.py
index 95e6222e78..3ed720b25a 100644
--- a/bin/tests/system/dnssec_nsec3/tests_dnssec_nsec3_delegation.py
+++ b/bin/tests/system/dnssec_nsec3/tests_dnssec_nsec3_delegation.py
@@ -9,14 +9,6 @@
 # See the COPYRIGHT file distributed with this work for additional
 # information regarding copyright ownership.

-from pathlib import Path
-
-import json
-
-from cryptography.hazmat.primitives import serialization
-from cryptography.hazmat.primitives.asymmetric import ec
-
-import dns.dnssec
 import dns.name
 import dns.rdataclass
 import dns.rdatatype
@@ -24,6 +16,8 @@ import pytest

 import isctest
 import isctest.mark
+import isctest.template
+import isctest.zone

 F055_ZONE = "f055.test."
 SAFE = f"safe.{F055_ZONE}"
@@ -33,44 +27,26 @@ ATTACK = f"a.{CUT}"
 CONTROL_ATTACK = f"a.{CONTROL}"
 SAFE_A = "198.51.100.55"
 FORGED_A = "192.0.2.55"
-AUTH = "10.53.0.1"
-RESOLVER = "10.53.0.2"
+AUTH = isctest.template.ANS3.ip
+RESOLVER = isctest.template.NS2.ip

 pytestmark = [
     isctest.mark.with_ecdsa_deterministic,
     pytest.mark.extra_artifacts(
         [
-            "ans*/keys.json",
+            "ans*/dsset-*",
+            "ans*/zones/*.db",
+            "ans*/zones/*.db.signed",
         ]
     ),
 ]


-def _make_key(zone: str):
-    private_key = ec.generate_private_key(ec.SECP256R1())
-    dnskey = dns.dnssec.make_dnskey(
-        private_key.public_key(),
-        algorithm="ECDSAP256SHA256",
-        flags=257,
-    )
-    ds = dns.dnssec.make_ds(dns.name.from_text(zone), dnskey, "SHA256")
-    private_pem = private_key.private_bytes(
-        encoding=serialization.Encoding.PEM,
-        format=serialization.PrivateFormat.PKCS8,
-        encryption_algorithm=serialization.NoEncryption(),
-    ).decode("ascii")
-    return {
-        "private_pem": private_pem,
-        "dnskey": dnskey.to_text(),
-        "ds": ds.to_text(),
-    }
-
-
 def bootstrap():
-    keys = {F055_ZONE: _make_key(F055_ZONE)}
-    Path("ans1/keys.json").write_text(json.dumps(keys, indent=2), encoding="ascii")
-    zone_dnskey = "".join(keys[F055_ZONE]["dnskey"].split()[3:])
-    return {"ZONE_DNSKEY": zone_dnskey}
+    zone = isctest.zone.Zone("f055.test.", isctest.template.ANS3, signed=True)
+    zone.configure(csk=True, sign_params="-3 -")
+
+    return {"trust_anchors": zone.trust_anchors()}


 def _query(server, qname, qtype):