From 761758b0b1814a122cda8c15fd5255f19d96d088 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Mon, 2 Sep 2024 17:48:22 +0200 Subject: [PATCH 01/12] Change dnssec-ksr key sorting Sort keys on algorithm, then keytag. This is more convenient for testing. (cherry picked from commit ea1fc5c47b1e242eaa43483a00bbb5922c4ad2d3) --- bin/dnssec/dnssec-ksr.c | 10 +++++++--- bin/tests/system/ksr/ns1/named.conf.in | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/bin/dnssec/dnssec-ksr.c b/bin/dnssec/dnssec-ksr.c index e0826f8153..5705842042 100644 --- a/bin/dnssec/dnssec-ksr.c +++ b/bin/dnssec/dnssec-ksr.c @@ -186,10 +186,14 @@ getkasp(ksr_ctx_t *ksr, dns_kasp_t **kasp) { } static int -keytag_cmp(const void *k1, const void *k2) { +keyalgtag_cmp(const void *k1, const void *k2) { dns_dnsseckey_t **key1 = (dns_dnsseckey_t **)k1; dns_dnsseckey_t **key2 = (dns_dnsseckey_t **)k2; - if (dst_key_id((*key1)->key) < dst_key_id((*key2)->key)) { + if (dst_key_alg((*key1)->key) < dst_key_alg((*key2)->key)) { + return (-1); + } else if (dst_key_alg((*key1)->key) > dst_key_alg((*key2)->key)) { + return (1); + } else if (dst_key_id((*key1)->key) < dst_key_id((*key2)->key)) { return (-1); } else if (dst_key_id((*key1)->key) > dst_key_id((*key2)->key)) { return (1); @@ -224,7 +228,7 @@ get_dnskeys(ksr_ctx_t *ksr, dns_dnsseckeylist_t *keys) { { keys_sorted[i] = dk; } - qsort(keys_sorted, n, sizeof(dns_dnsseckey_t *), keytag_cmp); + qsort(keys_sorted, n, sizeof(dns_dnsseckey_t *), keyalgtag_cmp); while (!ISC_LIST_EMPTY(keys_read)) { dns_dnsseckey_t *key = ISC_LIST_HEAD(keys_read); ISC_LIST_UNLINK(keys_read, key, link); diff --git a/bin/tests/system/ksr/ns1/named.conf.in b/bin/tests/system/ksr/ns1/named.conf.in index 9cd4ed6725..75710b42dc 100644 --- a/bin/tests/system/ksr/ns1/named.conf.in +++ b/bin/tests/system/ksr/ns1/named.conf.in @@ -79,9 +79,9 @@ dnssec-policy "no-cds" { dnssec-policy "two-tone" { offline-ksk yes; keys { - ksk lifetime unlimited algorithm @DEFAULT_ALGORITHM@; ksk lifetime unlimited algorithm @ALTERNATIVE_ALGORITHM@; - zsk lifetime P3M algorithm @DEFAULT_ALGORITHM@; zsk lifetime P5M algorithm @ALTERNATIVE_ALGORITHM@; + ksk lifetime unlimited algorithm @DEFAULT_ALGORITHM@; + zsk lifetime P3M algorithm @DEFAULT_ALGORITHM@; }; }; From 5d0144d006202c18f2bf62ec88bf214324524da2 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Mon, 2 Sep 2024 17:51:32 +0200 Subject: [PATCH 02/12] Introduce pytest kasp library Write initial pytest kasp library. This contains everything that is required for testing Offline KSK functionality with pytest. This includes: - addtime: adding a value to a timing metadata - get_timing_metdata: retrieve timing metadata from keyfile - get_metadata/get_keystate: retrieve metadata from statefile - get_keytag: retrieve keytag from base keyfile string - get_keyrole: get key role from statefile - dnskey_equals: compare DNSKEY record from file against a string - cds_equals: compare CDS derived from file against a string - zone_is_signed: wait until a zone is completely signed - dnssec_verify: verify a DNSSEC signed zone with dnssec-verify - check_dnssecstatus: check rndc dnssec -status output - check_signatures: check that signatures for a given RRset are correct - check_dnskeys: check that the published DNSKEY RRset is correct - check_cds: check that the published CDS RRset is correct - check_apex: check SOA, DNSKEY, CDNSKEY, and CDS RRset - check_subdomain: check an RRset below the apex (cherry picked from commit a3829990fdb67b646f41ef69d2d4ab3b5debbc24) --- bin/tests/system/isctest/__init__.py | 1 + bin/tests/system/isctest/kasp.py | 541 +++++++++++++++++++++++++++ 2 files changed, 542 insertions(+) create mode 100644 bin/tests/system/isctest/kasp.py diff --git a/bin/tests/system/isctest/__init__.py b/bin/tests/system/isctest/__init__.py index 5426e450fe..756f6b6b38 100644 --- a/bin/tests/system/isctest/__init__.py +++ b/bin/tests/system/isctest/__init__.py @@ -12,6 +12,7 @@ from . import check from . import instance from . import query +from . import kasp from . import name from . import rndc from . import run diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py new file mode 100644 index 0000000000..685250f291 --- /dev/null +++ b/bin/tests/system/isctest/kasp.py @@ -0,0 +1,541 @@ +# 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. + +import os +import time + +from datetime import datetime +from datetime import timedelta + +import dns +import isctest.log + + +DEFAULT_TTL = 300 + + +def _save_response(response, fname): + with open(fname, "w", encoding="utf-8") as file: + file.write(response.to_text()) + + +def _query(server, qname, qtype, outfile=None): + query = dns.message.make_query(qname, qtype, use_edns=True, want_dnssec=True) + try: + response = dns.query.tcp(query, server.ip, port=server.ports.dns, timeout=3) + except dns.exception.Timeout: + isctest.log.debug(f"query timeout for query {qname} {qtype} to {server.ip}") + return None + + if outfile is not None: + _save_response(response, outfile) + + return response + + +def addtime(value, plus): + # Get timing metadata from a value plus additional time. + # Convert "%Y%m%d%H%M%S" format to epoch seconds. + # Then, add the additional time (can be negative). + now = datetime.strptime(value, "%Y%m%d%H%M%S") + delta = timedelta(seconds=plus) + then = now + delta + return then.strftime("%Y%m%d%H%M%S") + + +def get_timing_metadata(key, metadata, keydir=None, offset=0, must_exist=True): + value = "0" + + if keydir is not None: + keyfile = "{}/{}.key".format(keydir, key) + else: + keyfile = "{}.key".format(key) + + with open(keyfile, "r", encoding="utf-8") as file: + for line in file: + if "; {}".format(metadata) in line: + value = line.split()[2] + break + + if must_exist: + assert int(value) > 0 + + if int(value) > 0: + return addtime(value, offset) + + return "0" + + +def get_metadata(key, metadata, keydir=None, must_exist=True): + if keydir is not None: + statefile = "{}/{}.state".format(keydir, key) + else: + statefile = "{}.state".format(key) + + value = "undefined" + with open(statefile, "r", encoding="utf-8") as file: + for line in file: + if f"{metadata}: " in line: + value = line.split()[1] + break + + if must_exist: + assert value != "undefined" + + return value + + +def get_keystate(key, metadata, keydir=None, must_exist=True): + + return get_metadata(key, metadata, keydir, must_exist) + + +def get_keytag(key): + return int(key[-5:]) + + +def get_keyrole(key, keydir=None): + ksk = "no" + zsk = "no" + + if keydir is not None: + statefile = "{}/{}.state".format(keydir, key) + else: + statefile = "{}.state".format(key) + + with open(statefile, "r", encoding="utf-8") as file: + for line in file: + if "KSK: " in line: + ksk = line.split()[1] + if "ZSK: " in line: + zsk = line.split()[1] + + return ksk == "yes", zsk == "yes" + + +def dnskey_equals(key, value, keydir=None, cdnskey=False): + if keydir is not None: + keyfile = f"{keydir}/{key}.key" + else: + keyfile = f"{key}.key" + + dnskey = value.split() + + if cdnskey: + # fourth element is the rrtype + assert dnskey[3] == "CDNSKEY" + dnskey[3] = "DNSKEY" + + dnskey_fromfile = [] + rdata = " ".join(dnskey[:7]) + + with open(keyfile, "r", encoding="utf-8") as file: + for line in file: + if f"{rdata}" in line: + dnskey_fromfile = line.split() + + pubkey_fromfile = "".join(dnskey_fromfile[7:]) + pubkey_fromwire = "".join(dnskey[7:]) + + return pubkey_fromfile == pubkey_fromwire + + +def cds_equals(key, value, alg, keydir=None): + if keydir is not None: + keyfile = f"{keydir}/{key}.key" + else: + keyfile = f"{key}.key" + + cds = value.split() + + dsfromkey_command = [ + *os.environ.get("DSFROMKEY").split(), + "-T", + "3600", + "-a", + alg, + "-C", + "-w", + keyfile, + ] + + out = isctest.run.cmd(dsfromkey_command, log_stdout=True) + dsfromkey = out.stdout.decode("utf-8").split() + index = 6 + while index < len(cds): + dsfromkey[index] = dsfromkey[index].lower() + index += 1 + + rdata_fromfile = " ".join(dsfromkey[:7]) + rdata_fromwire = " ".join(cds[:7]) + if rdata_fromfile != rdata_fromwire: + isctest.log.debug(f"CDS RDATA MISMATCH: {rdata_fromfile} - {rdata_fromwire}") + return False + + digest_fromfile = "".join(cds[7:]) + digest_fromwire = "".join(cds[7:]) + if digest_fromfile != digest_fromwire: + isctest.log.debug(f"CDS DIGEST MISMATCH: {digest_fromfile} - {digest_fromwire}") + return False + + return digest_fromfile == digest_fromwire + + +def zone_is_signed(server, zone): + addr = server.ip + fqdn = f"{zone}." + + # wait until zone is fully signed + signed = False + for _ in range(10): + response = _query(server, fqdn, dns.rdatatype.NSEC) + if not isinstance(response, dns.message.Message): + isctest.log.debug(f"no response for {fqdn} NSEC from {addr}") + elif response.rcode() != dns.rcode.NOERROR: + rcode = dns.rcode.to_text(response.rcode()) + isctest.log.debug(f"{rcode} response for {fqdn} NSEC from {addr}") + else: + has_nsec = False + has_rrsig = False + for rr in response.answer: + if not has_nsec: + has_nsec = rr.match( + dns.name.from_text(fqdn), + dns.rdataclass.IN, + dns.rdatatype.NSEC, + dns.rdatatype.NONE, + ) + if not has_rrsig: + has_rrsig = rr.match( + dns.name.from_text(fqdn), + dns.rdataclass.IN, + dns.rdatatype.RRSIG, + dns.rdatatype.NSEC, + ) + + if not has_nsec: + isctest.log.debug( + f"missing apex {fqdn} NSEC record in response from {addr}" + ) + if not has_rrsig: + isctest.log.debug( + f"missing {fqdn} NSEC signature in response from {addr}" + ) + + signed = has_nsec and has_rrsig + + if signed: + break + + time.sleep(1) + + assert signed + + +def dnssec_verify(server, zone): + # Check if zone if DNSSEC valid with dnssec-verify. + fqdn = f"{zone}." + transfer = _query(server, fqdn, dns.rdatatype.AXFR) + if not isinstance(transfer, dns.message.Message): + isctest.log.debug(f"no response for {fqdn} AXFR from {server.ip}") + elif transfer.rcode() != dns.rcode.NOERROR: + rcode = dns.rcode.to_text(transfer.rcode()) + isctest.log.debug(f"{rcode} response for {fqdn} AXFR from {server.ip}") + else: + zonefile = f"{zone}.axfr" + with open(zonefile, "w", encoding="utf-8") as file: + for rr in transfer.answer: + file.write(rr.to_text()) + file.write("\n") + + verify_command = [*os.environ.get("VERIFY").split(), "-z", "-o", zone, zonefile] + + isctest.run.cmd(verify_command) + + +def check_dnssecstatus(server, zone, keys, policy=None, view=None): + # Call rndc dnssec -status on 'server' for 'zone'. Expect 'policy' in + # the output. This is a loose verification, it just tests if the right + # policy name is returned, and if all expected keys are listed. + response = "" + if view is None: + response = server.rndc("dnssec -status {}".format(zone), log=False) + else: + response = server.rndc("dnssec -status {} in {}".format(zone, view), log=False) + + if policy is None: + assert "Zone does not have dnssec-policy" in response + return + + assert "dnssec-policy: {}".format(policy) in response + + for key in keys: + keytag = get_keytag(key) + assert "key: {}".format(keytag) in response + + +# pylint: disable=too-many-locals,too-many-branches +def _check_signatures(signatures, covers, fqdn, keys, keydir=None): + now = datetime.now().strftime("%Y%m%d%H%M%S") + numsigs = 0 + zrrsig = True + if covers in [dns.rdatatype.DNSKEY, dns.rdatatype.CDNSKEY, dns.rdatatype.CDS]: + zrrsig = False + krrsig = not zrrsig + + for key in keys: + keytag = get_keytag(key) + ksk, zsk = get_keyrole(key, keydir=keydir) + activate = get_timing_metadata(key, "Activate", keydir=keydir) + inactive = get_timing_metadata(key, "Inactive", keydir=keydir, must_exist=False) + + active = int(now) >= int(activate) + retired = int(inactive) != 0 and int(inactive) <= int(now) + signing = active and not retired + + if not signing: + for rrsig in signatures: + assert f"{keytag} {fqdn}" not in rrsig + continue + + if zrrsig and zsk: + has_rrsig = False + for rrsig in signatures: + if f"{keytag} {fqdn}" in rrsig: + has_rrsig = True + break + assert has_rrsig + numsigs += 1 + + if zrrsig and not zsk: + for rrsig in signatures: + assert f"{keytag} {fqdn}" not in rrsig + + if krrsig and ksk: + has_rrsig = False + for rrsig in signatures: + if f"{keytag} {fqdn}" in rrsig: + has_rrsig = True + break + assert has_rrsig + numsigs += 1 + + if krrsig and not ksk: + for rrsig in signatures: + assert f"{keytag} {fqdn}" not in rrsig + + return numsigs + + +# pylint: disable=too-many-arguments +def check_signatures(rrset, covers, fqdn, ksks, zsks, kskdir=None, zskdir=None): + # Check if signatures with covering type are signed with the right keys. + # The right keys are the ones that expect a signature and have the + # correct role. + numsigs = 0 + + signatures = [] + for rr in rrset: + for rdata in rr: + rdclass = dns.rdataclass.to_text(rr.rdclass) + rdtype = dns.rdatatype.to_text(rr.rdtype) + rrsig = f"{rr.name} {rr.ttl} {rdclass} {rdtype} {rdata}" + signatures.append(rrsig) + + numsigs += _check_signatures(signatures, covers, fqdn, ksks, keydir=kskdir) + numsigs += _check_signatures(signatures, covers, fqdn, zsks, keydir=zskdir) + + assert numsigs == len(signatures) + + +def _check_dnskeys(dnskeys, keys, keydir=None, cdnskey=False): + now = datetime.now().strftime("%Y%m%d%H%M%S") + numkeys = 0 + + publish_md = "Publish" + delete_md = "Delete" + if cdnskey: + publish_md = f"Sync{publish_md}" + delete_md = f"Sync{delete_md}" + + for key in keys: + publish = get_timing_metadata(key, publish_md, keydir=keydir) + delete = get_timing_metadata(key, delete_md, keydir=keydir, must_exist=False) + published = int(now) >= int(publish) + removed = int(delete) != 0 and int(delete) <= int(now) + + if not published or removed: + for dnskey in dnskeys: + assert not dnskey_equals(key, dnskey, keydir=keydir, cdnskey=cdnskey) + continue + + has_dnskey = False + for dnskey in dnskeys: + if dnskey_equals(key, dnskey, keydir=keydir, cdnskey=cdnskey): + has_dnskey = True + break + + assert has_dnskey + numkeys += 1 + + return numkeys + + +# pylint: disable=too-many-arguments +def check_dnskeys(rrset, ksks, zsks, kskdir=None, zskdir=None, cdnskey=False): + # Check if the correct DNSKEY records are published. If the current time + # is between the timing metadata 'publish' and 'delete', the key must have + # a DNSKEY record published. If 'cdnskey' is True, check against CDNSKEY + # records instead. + numkeys = 0 + + dnskeys = [] + for rr in rrset: + for rdata in rr: + rdclass = dns.rdataclass.to_text(rr.rdclass) + rdtype = dns.rdatatype.to_text(rr.rdtype) + dnskey = f"{rr.name} {rr.ttl} {rdclass} {rdtype} {rdata}" + dnskeys.append(dnskey) + + numkeys += _check_dnskeys(dnskeys, ksks, keydir=kskdir, cdnskey=cdnskey) + if not cdnskey: + numkeys += _check_dnskeys(dnskeys, zsks, keydir=zskdir) + + assert numkeys == len(dnskeys) + + +# pylint: disable=too-many-locals +def check_cds(rrset, keys, keydir=None): + # Check if the correct CDS records are published. If the current time + # is between the timing metadata 'publish' and 'delete', the key must have + # a DNSKEY record published. If 'cdnskey' is True, check against CDNSKEY + # records instead. + now = datetime.now().strftime("%Y%m%d%H%M%S") + numcds = 0 + + cdss = [] + for rr in rrset: + for rdata in rr: + rdclass = dns.rdataclass.to_text(rr.rdclass) + rdtype = dns.rdatatype.to_text(rr.rdtype) + cds = f"{rr.name} {rr.ttl} {rdclass} {rdtype} {rdata}" + cdss.append(cds) + + for key in keys: + ksk, _ = get_keyrole(key, keydir=keydir) + assert ksk + + publish = get_timing_metadata(key, "SyncPublish", keydir=keydir) + delete = get_timing_metadata(key, "SyncDelete", keydir=keydir, must_exist=False) + published = int(now) >= int(publish) + removed = int(delete) != 0 and int(delete) <= int(now) + if not published or removed: + for cds in cdss: + assert not cds_equals(key, cds, "SHA-256", keydir=keydir) + continue + + has_cds = False + for cds in cdss: + if cds_equals(key, cds, "SHA-256", keydir=keydir): + has_cds = True + break + + assert has_cds + numcds += 1 + + assert numcds == len(cdss) + + +def _query_rrset(server, fqdn, qtype): + response = _query(server, fqdn, qtype) + assert response.rcode() == dns.rcode.NOERROR + + rrs = [] + rrsigs = [] + for rrset in response.answer: + if rrset.match( + dns.name.from_text(fqdn), dns.rdataclass.IN, dns.rdatatype.RRSIG, qtype + ): + rrsigs.append(rrset) + elif rrset.match( + dns.name.from_text(fqdn), dns.rdataclass.IN, qtype, dns.rdatatype.NONE + ): + rrs.append(rrset) + else: + assert False + + return rrs, rrsigs + + +# pylint: disable=too-many-arguments +def check_apex(server, zone, ksks, zsks, kskdir=None, zskdir=None): + # Test the apex of a zone. This checks that the SOA and DNSKEY RRsets + # are signed correctly and with the appropriate keys. + fqdn = f"{zone}." + + # test dnskey query + dnskeys, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.DNSKEY) + assert len(dnskeys) > 0 + check_dnskeys(dnskeys, ksks, zsks, kskdir=kskdir, zskdir=zskdir) + assert len(rrsigs) > 0 + check_signatures( + rrsigs, dns.rdatatype.DNSKEY, fqdn, ksks, zsks, kskdir=kskdir, zskdir=zskdir + ) + + # test soa query + soa, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.SOA) + assert len(soa) == 1 + assert f"{zone}. {DEFAULT_TTL} IN SOA" in soa[0].to_text() + assert len(rrsigs) > 0 + check_signatures( + rrsigs, dns.rdatatype.SOA, fqdn, ksks, zsks, kskdir=kskdir, zskdir=zskdir + ) + + # test cdnskey query + cdnskeys, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.CDNSKEY) + assert len(cdnskeys) > 0 + check_dnskeys(cdnskeys, ksks, zsks, kskdir=kskdir, zskdir=zskdir, cdnskey=True) + assert len(rrsigs) > 0 + check_signatures( + rrsigs, dns.rdatatype.CDNSKEY, fqdn, ksks, zsks, kskdir=kskdir, zskdir=zskdir + ) + + # test cds query + cds, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.CDS) + assert len(cds) > 0 + check_cds(cds, ksks, keydir=kskdir) + assert len(rrsigs) > 0 + check_signatures( + rrsigs, dns.rdatatype.CDS, fqdn, ksks, zsks, kskdir=kskdir, zskdir=zskdir + ) + + +# pylint: disable=too-many-arguments +def check_subdomain(server, zone, ksks, zsks, kskdir=None, zskdir=None): + # Test an RRset below the apex and verify it is signed correctly. + fqdn = f"{zone}." + qname = f"a.{zone}." + qtype = dns.rdatatype.A + response = _query(server, qname, qtype) + assert response.rcode() == dns.rcode.NOERROR + + match = f"{qname} {DEFAULT_TTL} IN A 10.0.0.1" + rrsigs = [] + for rrset in response.answer: + if rrset.match( + dns.name.from_text(qname), dns.rdataclass.IN, dns.rdatatype.RRSIG, qtype + ): + rrsigs.append(rrset) + else: + assert match in rrset.to_text() + + assert len(rrsigs) > 0 + check_signatures(rrsigs, qtype, fqdn, ksks, zsks, kskdir=kskdir, zskdir=zskdir) From 288dce213a1ddbd6465a03a3833806516dc4c6ab Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Mon, 2 Sep 2024 17:36:47 +0200 Subject: [PATCH 03/12] Convert ksr system test to pytest Move all test cases from tests.sh to tests_ksr.py. The only test that is not moved is the check that key id's match expected keys. The shell-based system test checks two earlier set environment variables against each other that has become redundant in the pytest variant, because we now check the signed key response against a list of keys and for each key we take into account the timing metadata. So we already ensure that each published key is in the correct key bundle. (cherry picked from commit a15bf6704beddc7ef18609094e8f5c7505b3e3e2) --- bin/tests/system/ksr/clean.sh | 38 - bin/tests/system/ksr/ns1/setup.sh | 23 +- bin/tests/system/ksr/setup.sh | 2 - bin/tests/system/ksr/tests.sh | 1207 -------------------------- bin/tests/system/ksr/tests_ksr.py | 1100 +++++++++++++++++++++++ bin/tests/system/ksr/tests_sh_ksr.py | 14 - 6 files changed, 1102 insertions(+), 1282 deletions(-) delete mode 100644 bin/tests/system/ksr/clean.sh delete mode 100644 bin/tests/system/ksr/tests.sh create mode 100644 bin/tests/system/ksr/tests_ksr.py delete mode 100644 bin/tests/system/ksr/tests_sh_ksr.py diff --git a/bin/tests/system/ksr/clean.sh b/bin/tests/system/ksr/clean.sh deleted file mode 100644 index 4149685b88..0000000000 --- a/bin/tests/system/ksr/clean.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/sh - -# 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. - -set -e - -rm -f ./*.ksk* -rm -f ./*.zsk* -rm -f ./created.out -rm -f ./footer.* -rm -f ./now.out -rm -f ./ns1/*.db -rm -f ./ns1/*.db.jbk -rm -f ./ns1/*.db.signed -rm -f ./ns1/*.db.signed.jnl -rm -f ./ns1/K* -rm -f ./ns1/keygen.out.* -rm -f ./ns1/named.conf -rm -f ./ns1/named.memstats -rm -f ./ns1/named.run -rm -f ./python.out -rm -f ./settime.out.* -rm -f ./ksr.*.err.* -rm -f ./ksr.*.expect -rm -f ./ksr.*.expect.* -rm -f ./ksr.*.out.* - -rm -rf ./ns1/keydir -rm -rf ./ns1/offline diff --git a/bin/tests/system/ksr/ns1/setup.sh b/bin/tests/system/ksr/ns1/setup.sh index c17c43de2e..a04b01a23e 100644 --- a/bin/tests/system/ksr/ns1/setup.sh +++ b/bin/tests/system/ksr/ns1/setup.sh @@ -24,24 +24,5 @@ cp template.db.in past.test.db cp template.db.in future.test.db cp template.db.in last-bundle.test.db cp template.db.in in-the-middle.test.db - -# Create KSK for the various policies. -create_ksk() { - KSK=$($KEYGEN -l named.conf -fK -k $2 $1 2>keygen.out.$1) - num=0 - for ksk in $KSK; do - num=$(($num + 1)) - echo $ksk >"../${1}.ksk${num}.id" - cat "${ksk}.key" | grep -v ";.*" >"../$1.ksk$num" - mv "${ksk}.key" offline/ - mv "${ksk}.private" offline/ - mv "${ksk}.state" offline/ - done -} -create_ksk common.test common -create_ksk past.test common -create_ksk future.test common -create_ksk last-bundle.test common -create_ksk in-the-middle.test common -create_ksk unlimited.test unlimited -create_ksk two-tone.test two-tone +cp template.db.in unlimited.test.db +cp template.db.in two-tone.test.db diff --git a/bin/tests/system/ksr/setup.sh b/bin/tests/system/ksr/setup.sh index 2cfad07fb1..e200a4d434 100644 --- a/bin/tests/system/ksr/setup.sh +++ b/bin/tests/system/ksr/setup.sh @@ -16,8 +16,6 @@ set -e -$SHELL clean.sh - copy_setports ns1/named.conf.in ns1/named.conf ( diff --git a/bin/tests/system/ksr/tests.sh b/bin/tests/system/ksr/tests.sh deleted file mode 100644 index 4fb9f9fac5..0000000000 --- a/bin/tests/system/ksr/tests.sh +++ /dev/null @@ -1,1207 +0,0 @@ -#!/bin/sh - -# 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. - -# shellcheck source=conf.sh -. ../conf.sh -# shellcheck source=kasp.sh -. ../kasp.sh - -set -e - -RNDCCMD="$RNDC -c ../_common/rndc.conf -p ${CONTROLPORT} -s" - -CDS_SHA1="no" -CDS_SHA256="yes" -CDS_SHA384="no" -CDNSKEY="yes" - -status=0 -n=0 - -# Get timing metadata from a value plus additional time. -# $1: Value -# $2: Additional time -addtime() { - if [ -x "$PYTHON" ]; then - # Convert "%Y%m%d%H%M%S" format to epoch seconds. - # Then, add the additional time (can be negative). - _value=$1 - _plus=$2 - $PYTHON >python.out <created.out || return 1 - created=$(awk '{print $3}' /dev/null || return 1 - grep "Length: $size" $statefile >/dev/null || return 1 - grep "Lifetime: $lifetime" $statefile >/dev/null || return 1 - grep "KSK: no" $statefile >/dev/null || return 1 - grep "ZSK: yes" $statefile >/dev/null || return 1 - grep "Published: $published" $statefile >/dev/null || return 1 - grep "Active: $active" $statefile >/dev/null || return 1 - grep "Retired: $retired" $statefile >/dev/null || return 1 - grep "Removed: $removed" $statefile >/dev/null || return 1 - - inception=$((inception + lifetime)) - num=$((num + 1)) - - # Save some information for testing - cp ${dir}/${key}.key ${key}.key.expect - cp ${dir}/${key}.private ${key}.private.expect - cp ${dir}/${key}.state ${key}.state.expect - cat ${dir}/${key}.key | grep -v ";.*" >"${zone}.${alg}.zsk${num}" - echo $key >"${zone}.${alg}.zsk${num}.id" - done - - return 0 -) - -# Print the DNSKEY records for zone $1, which have keys listed in file $5 -# that match the keys with numbers $2 and $3, and match algorithm number $4, -# sorted by keytag. -print_dnskeys() { - for key in $(cat $5 | sort); do - for num in $2 $3; do - zsk=$(cat $1.$4.zsk$num.id) - if [ "$key" = "$zsk" ]; then - cat $1.$4.zsk$num >>ksr.request.expect.$n - fi - done - done -} -# Call the dnssec-ksr command: -# ksr [options] -ksr() { - $KSR -l ns1/named.conf -k "$@" -} - -# Unknown action. -n=$((n + 1)) -echo_i "check that 'dnssec-ksr' errors on unknown action ($n)" -ret=0 -ksr common foobar common.test >ksr.foobar.out.$n 2>&1 && ret=1 -grep "dnssec-ksr: fatal: unknown command 'foobar'" ksr.foobar.out.$n >/dev/null || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Key generation: common -set_zsk() { - ALG=$1 - SIZE=$2 - LIFETIME=$3 -} - -n=$((n + 1)) -echo_i "check that 'dnssec-ksr keygen' errors on missing end date ($n)" -ret=0 -ksr common keygen common.test >ksr.keygen.out.$n 2>&1 && ret=1 -grep "dnssec-ksr: fatal: keygen requires an end date" ksr.keygen.out.$n >/dev/null || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -echo_i "check that 'dnssec-ksr keygen' pregenerates right amount of keys in the common case ($n)" -ret=0 -ksr common -K ns1 -i now -e +1y keygen common.test >ksr.keygen.out.$n 2>&1 || ret=1 -num=$(cat ksr.keygen.out.$n | wc -l) -[ $num -eq 2 ] || ret=1 -set_zsk $DEFAULT_ALGORITHM_NUMBER $DEFAULT_BITS 16070400 -ksr_check_keys common.test ns1 0 || ret=1 -cp ksr.keygen.out.$n ksr.keygen.out.expect -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) -# save now time -key=$(cat common.test.$DEFAULT_ALGORITHM_NUMBER.zsk1.id) -grep "; Created:" "ns1/${key}.key" >now.out || ret=1 -now=$(awk '{print $3}' ksr.keygen.out.$n 2>&1 || ret=1 -diff -w ksr.keygen.out.expect ksr.keygen.out.$n >/dev/null || ret=1 -for key in $(cat ksr.keygen.out.$n); do - # Ensure the files are not modified. - diff ns1/${key}.key ${key}.key.expect >/dev/null || ret=1 - diff ns1/${key}.private ${key}.private.expect >/dev/null || ret=1 - diff ns1/${key}.state ${key}.state.expect >/dev/null || ret=1 -done -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Create request: common -n=$((n + 1)) -echo_i "check that 'dnssec-ksr request' errors on missing end date ($n)" -ret=0 -ksr common -K ns1 request common.test >ksr.request.out.$n 2>&1 && ret=1 -grep "dnssec-ksr: fatal: request requires an end date" ksr.request.out.$n >/dev/null || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -echo_i "check that 'dnssec-ksr request' creates correct KSR in the common case ($n)" -ret=0 -ksr common -K ns1 -i $now -e +1y request common.test >ksr.request.out.$n 2>&1 || ret=1 -# Bundle 1: KSK + ZSK1 -key=$(cat common.test.$DEFAULT_ALGORITHM_NUMBER.zsk1.id) -inception=$(cat ns1/$key.state | grep "Generated" | cut -d' ' -f 2-) -echo ";; KeySigningRequest 1.0 $inception" >ksr.request.expect.$n -cat common.test.$DEFAULT_ALGORITHM_NUMBER.zsk1 >>ksr.request.expect.$n -# Bundle 2: KSK + ZSK1 + ZSK2 -key=$(cat common.test.$DEFAULT_ALGORITHM_NUMBER.zsk2.id) -inception=$(cat ns1/$key.state | grep "Published" | cut -d' ' -f 2-) -echo ";; KeySigningRequest 1.0 $inception" >>ksr.request.expect.$n -print_dnskeys common.test 1 2 $DEFAULT_ALGORITHM_NUMBER ksr.keygen.out.expect -# Bundle 3: KSK + ZSK2 -key=$(cat common.test.$DEFAULT_ALGORITHM_NUMBER.zsk1.id) -inception=$(cat ns1/$key.state | grep "Removed" | cut -d' ' -f 2-) -echo ";; KeySigningRequest 1.0 $inception" >>ksr.request.expect.$n -cat common.test.$DEFAULT_ALGORITHM_NUMBER.zsk2 >>ksr.request.expect.$n -# Footer -cp ksr.request.expect.$n ksr.request.expect.base -grep ";; KeySigningRequest 1.0 generated at" ksr.request.out.$n >footer.$n || ret=1 -cat footer.$n >>ksr.request.expect.$n -# Check if request output is the same as expected. -diff -w ksr.request.out.$n ksr.request.expect.$n >/dev/null || ret=1 -# Save request for ksr sign operation. -cp ksr.request.expect.$n ksr.request.expect -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Sign request: common -n=$((n + 1)) -echo_i "check that 'dnssec-ksr sign' errors on missing KSR file ($n)" -ret=0 -ksr common -K ns1 -i $now -e +1y sign common.test >ksr.sign.out.$n 2>&1 && ret=1 -grep "dnssec-ksr: fatal: 'sign' requires a KSR file" ksr.sign.out.$n >/dev/null || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -echo_i "check that 'dnssec-ksr sign' creates correct SKR in the common case ($n)" -ret=0 -ksr common -K ns1 -i $now -e +1y -K ns1/offline -f ksr.request.expect sign common.test >ksr.sign.out.$n 2>&1 || ret=1 - -_update_expected_zsks() { - zsk=$((zsk + 1)) - next=$((next + 1)) - inception=$rollover_done - if [ "$next" -le "$numzsks" ]; then - key1="${zone}.${DEFAULT_ALGORITHM_NUMBER}.zsk${zsk}" - key2="${zone}.${DEFAULT_ALGORITHM_NUMBER}.zsk${next}" - zsk1=$(cat $key1.id) - zsk2=$(cat $key2.id) - rollover_start=$(cat ns1/$zsk2.state | grep "Published" | awk '{print $2}') - rollover_done=$(cat ns1/$zsk1.state | grep "Removed" | awk '{print $2}') - else - # No more expected rollovers. - key1="${zone}.${DEFAULT_ALGORITHM_NUMBER}.zsk${zsk}" - zsk1=$(cat $key1.id) - rollover_start=$((end + 1)) - rollover_done=$((end + 1)) - fi -} - -check_skr() { - _ret=0 - zone=$1 - dir=$2 - file=$3 - start=$4 - end=$5 - numzsks=$6 - cds1=$($DSFROMKEY -T 3600 -a SHA-1 -C -w $dir/$(cat "${zone}.ksk1.id")) - cds2=$($DSFROMKEY -T 3600 -a SHA-256 -C -w $dir/$(cat "${zone}.ksk1.id")) - cds4=$($DSFROMKEY -T 3600 -a SHA-384 -C -w $dir/$(cat "${zone}.ksk1.id")) - cdnskey=$(awk '{sub(/DNSKEY/,"CDNSKEY")}1' <${zone}.ksk1) - - echo_i "check skr: zone $1 file $2 from $3 to $4 num-zsk $5" - - # Initial state: not in a rollover, expect a SignedKeyResponse header - # on the first line, start with the first ZSK (set zsk=0 so when we - # call _update_expected_zsks, zsk is set to 1. - rollover=0 - expect="header" - zsk=0 - next=1 - rollover_done=$start - _update_expected_zsks - - echo_i "check skr: inception $inception rollover-start $rollover_start rollover-done $rollover_done" - - lineno=0 - complete=0 - while IFS= read -r line; do - # A single signed key response may consist of: - # ;; SignedKeyResponse (header) - # ;; DNSKEY 257 (ksk) - # ;; one or two (during rollover) DNSKEY 256 (zsk1, zsk2) - # ;; RRSIG(DNSKEY) (rrsig-dnskey) - # ;; CDNSKEY (cdnskey) - # ;; RRSIG(CDNSKEY) (rrsig-cdnskey) - # ;; CDS (cds) - # ;; RRSIG(CDS) (rrsig-cds) - err=0 - lineno=$((lineno + 1)) - - # skip empty lines - if [ -z "$line" ]; then - continue - fi - - if [ "$expect" = "header" ]; then - expected=";; SignedKeyResponse 1.0 $inception" - echo $line | grep "$expected" >/dev/null || err=1 - next_inception=$(addtime $inception 777600) - expect="ksk" - elif [ "$expect" = "ksk" ]; then - expected="$(cat ${zone}.ksk1)" - echo $line | grep "$expected" >/dev/null || err=1 - expect="zsk1" - elif [ "$expect" = "cdnskey" ]; then - expected="$cdnskey" - echo $line | grep "$expected" >/dev/null || err=1 - expect="rrsig-cdnskey" - elif [ "$expect" = "cds1" ]; then - expected="$cds1" - echo $line | grep "$expected" >/dev/null || err=1 - if [ "$CDS_SHA256" = "yes" ]; then - expect="cds2" - elif [ "$CDS_SHA384" = "yes" ]; then - expect="cds4" - else - expect="rrsig-cds" - fi - elif [ "$expect" = "cds2" ]; then - expected="$cds2" - echo $line | grep "$expected" >/dev/null || err=1 - if [ "$CDS_SHA384" = "yes" ]; then - expect="cds4" - else - expect="rrsig-cds" - fi - elif [ "$expect" = "cds4" ]; then - expected="$cds4" - echo $line | grep "$expected" >/dev/null || err=1 - expect="rrsig-cds" - elif [ "$expect" = "zsk1" ]; then - expected="$(cat $key1)" - echo $line | grep "$expected" >/dev/null || err=1 - expect="rrsig-dnskey" - [ "$rollover" -eq 1 ] && expect="zsk2" - elif [ "$expect" = "zsk2" ]; then - expected="$(cat $key2)" - echo $line | grep "$expected" >/dev/null || err=1 - expect="rrsig-dnskey" - elif [ "$expect" = "rrsig-dnskey" ]; then - exp=$(addtime $inception 1209600) # signature-validity 14 days - inc=$(addtime $inception -3600) # adjust for one hour clock skew - expected="${zone}. 3600 IN RRSIG DNSKEY 13 2 3600 $exp $inc" - echo $line | grep "$expected" >/dev/null || err=1 - if [ "$CDNSKEY" = "yes" ]; then - expect="cdnskey" - elif [ "$CDS_SHA1" = "yes" ]; then - expect="cds1" - elif [ "$CDS_SHA256" = "yes" ]; then - expect="cds2" - elif [ "$CDS_SHA384" = "yes" ]; then - expect="cds4" - else - complete=1 - fi - elif [ "$expect" = "rrsig-cdnskey" ]; then - exp=$(addtime $inception 1209600) # signature-validity 14 days - inc=$(addtime $inception -3600) # adjust for one hour clock skew - expected="${zone}. 3600 IN RRSIG CDNSKEY 13 2 3600 $exp $inc" - echo $line | grep "$expected" >/dev/null || err=1 - if [ "$CDS_SHA1" = "yes" ]; then - expect="cds1" - elif [ "$CDS_SHA256" = "yes" ]; then - expect="cds2" - elif [ "$CDS_SHA384" = "yes" ]; then - expect="cds4" - else - complete=1 - fi - elif [ "$expect" = "rrsig-cds" ]; then - exp=$(addtime $inception 1209600) # signature-validity 14 days - inc=$(addtime $inception -3600) # adjust for one hour clock skew - expected="${zone}. 3600 IN RRSIG CDS 13 2 3600 $exp $inc" - echo $line | grep "$expected" >/dev/null || err=1 - complete=1 - elif [ "$expect" = "footer" ]; then - expected=";; SignedKeyResponse 1.0 generated at" - echo "$(echo $line | tr -s ' ')" | grep "$expected" >/dev/null || err=1 - - expect="eof" - elif [ "$expect" = "eof" ]; then - expected="EOF" - echo_i "failed: expected EOF" - err=1 - else - echo_i "failed: bad expect value $expect" - err=1 - fi - - echo "$(echo $line | tr -s ' ')" | grep "$expected" >/dev/null || err=1 - if [ "$err" -ne 0 ]; then - echo_i "unexpected data on line $lineno:" - echo_i "line: $(echo $line | tr -s ' ')" - echo_i "expected: $expected" - fi - - if [ "$complete" -eq 1 ]; then - inception=$next_inception - expect="header" - - # Update rollover status if required. - if [ "$inception" -ge "$end" ]; then - expect="footer" - elif [ "$inception" -ge "$rollover_done" ]; then - [ "$rollover" -eq 1 ] && inception=$rollover_done - rollover=0 - _update_expected_zsks - elif [ "$inception" -ge "$rollover_start" ]; then - [ "$rollover" -eq 0 ] && inception=$rollover_start - rollover=1 - # Keys will be sorted, so during a rollover a key with a - # lower keytag will be printed first. Update key1/key2 and - # zsk1/zsk2 accordingly. - id1=$(keyfile_to_key_id "$zsk1") - id2=$(keyfile_to_key_id "$zsk2") - if [ $id1 -gt $id2 ]; then - key1="${zone}.${DEFAULT_ALGORITHM_NUMBER}.zsk${next}" - key2="${zone}.${DEFAULT_ALGORITHM_NUMBER}.zsk${zsk}" - zsk1=$(cat $key1.id) - zsk2=$(cat $key2.id) - fi - fi - complete=0 - fi - - _ret=$((_ret + err)) - test "$_ret" -eq 0 || exit $_ret - done <$file - - return $_ret -} - -zsk1=$(cat common.test.$DEFAULT_ALGORITHM_NUMBER.zsk1.id) -start=$(cat ns1/$zsk1.state | grep "Generated" | awk '{print $2}') -end=$(addtime $start 31536000) # one year -check_skr "common.test" "ns1/offline" "ksr.sign.out.$n" $start $end 2 || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Key generation: common (2) -n=$((n + 1)) -echo_i "check that 'dnssec-ksr keygen' pregenerates keys in the given key-directory ($n)" -ret=0 -ksr common -K ns1/keydir -e +1y keygen common.test >ksr.keygen.out.$n 2>&1 || ret=1 -num=$(cat ksr.keygen.out.$n | wc -l) -[ $num -eq 2 ] || ret=1 -set_zsk $DEFAULT_ALGORITHM_NUMBER $DEFAULT_BITS 16070400 -ksr_check_keys common.test ns1/keydir 0 || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -echo_i "check that 'dnssec-ksr keygen' selects generates only necessary keys for overlapping time bundle ($n)" -ret=0 -ksr common -K ns1 -e +2y -v 1 keygen common.test >ksr.keygen.out.$n 2>&1 || ret=1 -num=$(cat ksr.keygen.out.$n | wc -l) -[ $num -eq 4 ] || ret=1 -# 2 selected, 2 generated -num=$(grep "Selecting" ksr.keygen.out.$n | wc -l) -[ $num -eq 2 ] || ret=1 -num=$(grep "Generating" ksr.keygen.out.$n | wc -l) -[ $num -eq 2 ] || ret=1 -cp ksr.keygen.out.$n ksr.keygen.out.expect -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -echo_i "run 'dnssec-ksr keygen' again with verbosity 0 ($n)" -ret=0 -ksr common -K ns1 -i $now -e +2y keygen common.test >ksr.keygen.out.$n 2>&1 || ret=1 -num=$(cat ksr.keygen.out.$n | wc -l) -[ $num -eq 4 ] || ret=1 -set_zsk $DEFAULT_ALGORITHM_NUMBER $DEFAULT_BITS 16070400 -ksr_check_keys common.test ns1 0 || ret=1 -cp ksr.keygen.out.$n ksr.keygen.out.expect -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Create request: common (2) -n=$((n + 1)) -echo_i "check that 'dnssec-ksr request' creates correct KSR if the interval is shorter ($n)" -ret=0 -ksr common -K ns1 -i $now -e +1y request common.test >ksr.request.out.$n 2>&1 || ret=1 -# Same as earlier. -cp ksr.request.expect.base ksr.request.expect.$n -grep ";; KeySigningRequest 1.0 generated at" ksr.request.out.$n >footer.$n || ret=1 -cat footer.$n >>ksr.request.expect.$n -diff -w ksr.request.out.$n ksr.request.expect.$n >/dev/null || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -echo_i "check that 'dnssec-ksr request' creates correct KSR with new interval ($n)" -ret=0 -ksr common -K ns1 -i $now -e +2y request common.test >ksr.request.out.$n 2>&1 || ret=1 -cp ksr.request.expect.base ksr.request.expect.$n -# Bundle 4: KSK + ZSK2 + ZSK3 -key=$(cat common.test.$DEFAULT_ALGORITHM_NUMBER.zsk3.id) -inception=$(cat ns1/$key.state | grep "Published" | cut -d' ' -f 2-) -echo ";; KeySigningRequest 1.0 $inception" >>ksr.request.expect.$n -print_dnskeys common.test 2 3 $DEFAULT_ALGORITHM_NUMBER ksr.keygen.out.expect -# Bundle 5: KSK + ZSK3 -key=$(cat common.test.$DEFAULT_ALGORITHM_NUMBER.zsk2.id) -inception=$(cat ns1/$key.state | grep "Removed" | cut -d' ' -f 2-) -echo ";; KeySigningRequest 1.0 $inception" >>ksr.request.expect.$n -cat common.test.$DEFAULT_ALGORITHM_NUMBER.zsk3 >>ksr.request.expect.$n -# Bundle 6: KSK + ZSK3 + ZSK4 -key=$(cat common.test.$DEFAULT_ALGORITHM_NUMBER.zsk4.id) -inception=$(cat ns1/$key.state | grep "Published" | cut -d' ' -f 2-) -echo ";; KeySigningRequest 1.0 $inception" >>ksr.request.expect.$n -print_dnskeys common.test 3 4 $DEFAULT_ALGORITHM_NUMBER ksr.keygen.out.expect -# Bundle 7: KSK + ZSK4 -key=$(cat common.test.$DEFAULT_ALGORITHM_NUMBER.zsk3.id) -inception=$(cat ns1/$key.state | grep "Removed" | cut -d' ' -f 2-) -echo ";; KeySigningRequest 1.0 $inception" >>ksr.request.expect.$n -cat common.test.$DEFAULT_ALGORITHM_NUMBER.zsk4 >>ksr.request.expect.$n -# Footer -cp ksr.request.expect.$n ksr.request.expect.base -grep ";; KeySigningRequest 1.0 generated at" ksr.request.out.$n >footer.$n || ret=1 -cat footer.$n >>ksr.request.expect.$n -diff -w ksr.request.out.$n ksr.request.expect.$n >/dev/null || ret=1 -# Save request for ksr sign operation. -cp ksr.request.expect.$n ksr.request.expect -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -echo_i "check that 'dnssec-ksr request' errors if there are not enough keys ($n)" -ret=0 -ksr common -K ns1 -i $now -e +3y request common.test >ksr.request.out.$n 2>ksr.request.err.$n && ret=1 -grep "dnssec-ksr: fatal: no common.test/ECDSAP256SHA256 zsk key pair found for bundle" ksr.request.err.$n >/dev/null || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Sign request: common (2) -n=$((n + 1)) -echo_i "check that 'dnssec-ksr sign' creates correct SKR with the new interval ($n)" -ret=0 -ksr common -K ns1 -i $now -e +2y -K ns1/offline -f ksr.request.expect sign common.test >ksr.sign.out.$n 2>&1 || ret=1 -start=$(cat ns1/$zsk1.state | grep "Generated" | awk '{print $2}') -end=$(addtime $start 63072000) # two years -check_skr "common.test" "ns1/offline" "ksr.sign.out.$n" $start $end 4 || ret=1 -# Save response for skr import operation. -cp ksr.sign.out.$n ns1/common.test.skr -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Add zone: common -n=$((n + 1)) -echo_i "add zone 'common.test' ($n)" -ret=0 -$RNDCCMD 10.53.0.1 addzone 'common.test { type primary; file "common.test.db"; dnssec-policy common; };' 2>&1 | sed 's/^/I:ns1 /' || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Import skr: common -n=$((n + 1)) -echo_i "import ksr to zone 'common.test' ($n)" -ret=0 -sleep 2 -$RNDCCMD 10.53.0.1 skr -import common.test.skr common.test 2>&1 | sed 's/^/I:ns1 /' || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Test that common.test is signed and uses the right DNSKEY and RRSIG records. -n=$((n + 1)) -echo_i "test zone 'common.test' is correctly signed ($n)" -ret=0 - -set_zone "common.test" -set_policy "common" "4" "3600" -set_server "ns1" "10.53.0.1" -# Only ZSKs -set_keyrole "KEY1" "zsk" -set_keylifetime "KEY1" "16070400" -set_keyalgorithm "KEY1" "13" "ECDSAP256SHA256" "256" -set_keysigning "KEY1" "no" -set_zonesigning "KEY1" "yes" -set_keystate "KEY1" "GOAL" "omnipresent" -set_keystate "KEY1" "STATE_DNSKEY" "omnipresent" -set_keystate "KEY1" "STATE_ZRRSIG" "rumoured" - -set_keyrole "KEY2" "zsk" -set_keylifetime "KEY2" "16070400" -set_keyalgorithm "KEY2" "13" "ECDSAP256SHA256" "256" -set_keysigning "KEY2" "no" -set_zonesigning "KEY2" "no" -set_keystate "KEY2" "GOAL" "hidden" -set_keystate "KEY2" "STATE_DNSKEY" "hidden" -set_keystate "KEY2" "STATE_ZRRSIG" "hidden" - -set_keyrole "KEY3" "zsk" -set_keylifetime "KEY3" "16070400" -set_keyalgorithm "KEY3" "13" "ECDSAP256SHA256" "256" -set_keysigning "KEY3" "no" -set_zonesigning "KEY3" "no" -set_keystate "KEY3" "GOAL" "hidden" -set_keystate "KEY3" "STATE_DNSKEY" "hidden" -set_keystate "KEY3" "STATE_ZRRSIG" "hidden" - -set_keyrole "KEY4" "zsk" -set_keylifetime "KEY4" "16070400" -set_keyalgorithm "KEY4" "13" "ECDSAP256SHA256" "256" -set_keysigning "KEY4" "no" -set_zonesigning "KEY4" "no" -set_keystate "KEY4" "GOAL" "hidden" -set_keystate "KEY4" "STATE_DNSKEY" "hidden" -set_keystate "KEY4" "STATE_ZRRSIG" "hidden" - -MAXDEPTH=1 -check_keys -check_dnssecstatus "$SERVER" "$POLICY" "$ZONE" -check_subdomain -dnssec_verify - -# For checking the apex, we need to store the expected KSK metadata. -key_clear "KEY2" -key_clear "KEY3" -key_clear "KEY4" - -set_policy "common" "1" "3600" -set_server "ns1/offline" "10.53.0.1" -set_keyrole "KEY2" "ksk" -set_keylifetime "KEY2" "0" -set_keyalgorithm "KEY2" "13" "ECDSAP256SHA256" "256" -set_keysigning "KEY2" "yes" -set_zonesigning "KEY2" "no" -check_keys "keep" - -DIR="ns1" -set_keystate "KEY2" "STATE_DNSKEY" "omnipresent" -set_keystate "KEY2" "STATE_KRRSIG" "omnipresent" -set_keystate "KEY2" "STATE_DS" "omnipresent" -check_apex - -# Check that key id's match expected keys -n=$((n + 1)) -zsk1=$(cat common.test.$DEFAULT_ALGORITHM_NUMBER.zsk1.id) -key1=$(key_get "KEY1" BASEFILE) -echo_i "check that published zsk $zsk1 matches first key $key1 in bundle ($n)" -ret=0 -[ "ns1/$zsk1" = "$key1" ] || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -ksk=$(cat common.test.ksk1.id) -key2=$(key_get "KEY2" BASEFILE) -echo_i "check that published ksk $ksk matches ksk $key2 ($n)" -ret=0 -[ "ns1/offline/$ksk" = "$key2" ] || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Key generation: last-bundle -n=$((n + 1)) -echo_i "generate keys for testing an SKR that is in the last bundle ($n)" -ret=0 -ksr common -K ns1 -i -1y -e +1d keygen last-bundle.test >ksr.keygen.out.$n 2>&1 || ret=1 -num=$(cat ksr.keygen.out.$n | wc -l) -[ $num -eq 2 ] || ret=1 -set_zsk $DEFAULT_ALGORITHM_NUMBER $DEFAULT_BITS 16070400 -ksr_check_keys last-bundle.test ns1 -31536000 || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) -# Create request: last-bundle -n=$((n + 1)) -echo_i "create ksr for last bundle test ($n)" -ret=0 -ksr common -K ns1 -i -1y -e +1d request last-bundle.test >ksr.request.out.$n 2>&1 || ret=1 -cp ksr.request.out.$n last-bundle.test.ksr -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) -# Sign request: last-bundle -n=$((n + 1)) -echo_i "create skr for last bundle test ($n)" -ret=0 -ksr common -i -1y -e +1d -K ns1/offline -f last-bundle.test.ksr sign last-bundle.test >ksr.sign.out.$n 2>&1 || ret=1 -cp ksr.sign.out.$n ns1/last-bundle.test.skr -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) -# Add zone: last-bundle -n=$((n + 1)) -echo_i "add zone 'last-bundle.test' ($n)" -ret=0 -$RNDCCMD 10.53.0.1 addzone 'last-bundle.test { type primary; file "last-bundle.test.db"; dnssec-policy common; };' 2>&1 | sed 's/^/I:ns1 /' || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) -# Import skr: last-bundle -n=$((n + 1)) -echo_i "import ksr to zone 'last-bundle.test' ($n)" -ret=0 -sleep 2 -$RNDCCMD 10.53.0.1 skr -import last-bundle.test.skr last-bundle.test 2>&1 | sed 's/^/I:ns1 /' || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Test that last-bundle.test is signed and uses the right DNSKEY and RRSIG records. -n=$((n + 1)) -echo_i "test zone 'last-bundle.test' is correctly signed ($n)" -ret=0 - -set_zone "last-bundle.test" -set_policy "common" "2" "3600" -set_server "ns1" "10.53.0.1" -# Only ZSKs -key_clear "KEY1" -set_keyrole "KEY1" "zsk" -set_keylifetime "KEY1" "16070400" -set_keyalgorithm "KEY1" "13" "ECDSAP256SHA256" "256" -set_keysigning "KEY1" "no" -set_zonesigning "KEY1" "yes" -set_keystate "KEY1" "GOAL" "omnipresent" -set_keystate "KEY1" "STATE_DNSKEY" "omnipresent" -set_keystate "KEY1" "STATE_ZRRSIG" "omnipresent" - -key_clear "KEY2" -set_keyrole "KEY2" "zsk" -set_keylifetime "KEY2" "16070400" -set_keyalgorithm "KEY2" "13" "ECDSAP256SHA256" "256" -set_keysigning "KEY2" "no" -set_zonesigning "KEY2" "no" -set_keystate "KEY2" "GOAL" "hidden" -set_keystate "KEY2" "STATE_DNSKEY" "hidden" -set_keystate "KEY2" "STATE_ZRRSIG" "hidden" - -MAXDEPTH=1 -check_keys -check_dnssecstatus "$SERVER" "$POLICY" "$ZONE" -check_subdomain -dnssec_verify - -# For checking the apex, we need to store the expected KSK metadata. -key_clear "KEY2" -key_clear "KEY3" -key_clear "KEY4" - -set_policy "common" "1" "3600" -set_server "ns1/offline" "10.53.0.1" -set_keyrole "KEY2" "ksk" -set_keylifetime "KEY2" "0" -set_keyalgorithm "KEY2" "13" "ECDSAP256SHA256" "256" -set_keysigning "KEY2" "yes" -set_zonesigning "KEY2" "no" -check_keys "keep" - -DIR="ns1" -set_keystate "KEY2" "STATE_DNSKEY" "omnipresent" -set_keystate "KEY2" "STATE_KRRSIG" "omnipresent" -set_keystate "KEY2" "STATE_DS" "omnipresent" -check_apex - -# Check that key id's match expected keys -n=$((n + 1)) -zsk2=$(cat last-bundle.test.$DEFAULT_ALGORITHM_NUMBER.zsk2.id) -key1=$(key_get "KEY1" BASEFILE) -echo_i "check that published zsk $zsk2 matches first key $key1 in bundle ($n)" -ret=0 -[ "ns1/$zsk2" = "$key1" ] || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -ksk=$(cat last-bundle.test.ksk1.id) -key2=$(key_get "KEY2" BASEFILE) -echo_i "check that published ksk $ksk matches ksk $key2 ($n)" -ret=0 -[ "ns1/offline/$ksk" = "$key2" ] || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -echo_i "check that last bundle warning is logged ($n)" -wait_for_log 3 "zone last-bundle.test/IN (signed): zone_rekey: last bundle in skr, please import new skr file" ns1/named.run || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Key generation: in-the-middle -n=$((n + 1)) -echo_i "generate keys for testing an SKR that is in the middle ($n)" -ret=0 -ksr common -K ns1 -i -1y -e +1y keygen in-the-middle.test >ksr.keygen.out.$n 2>&1 || ret=1 -num=$(cat ksr.keygen.out.$n | wc -l) -[ $num -eq 4 ] || ret=1 -set_zsk $DEFAULT_ALGORITHM_NUMBER $DEFAULT_BITS 16070400 -ksr_check_keys in-the-middle.test ns1 -31536000 || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) -# Create request: in-the-middle -n=$((n + 1)) -echo_i "create ksr for in the middle test ($n)" -ret=0 -ksr common -K ns1 -i -1y -e +1y request in-the-middle.test >ksr.request.out.$n 2>&1 || ret=1 -cp ksr.request.out.$n in-the-middle.test.ksr -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) -# Sign request: in-the-middle -n=$((n + 1)) -echo_i "create skr for in the middle test ($n)" -ret=0 -ksr common -i -1y -e +1y -K ns1/offline -f in-the-middle.test.ksr sign in-the-middle.test >ksr.sign.out.$n 2>&1 || ret=1 -cp ksr.sign.out.$n ns1/in-the-middle.test.skr -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) -# Add zone: in-the-middle -n=$((n + 1)) -echo_i "add zone 'in-the-middle.test' ($n)" -ret=0 -$RNDCCMD 10.53.0.1 addzone 'in-the-middle.test { type primary; file "in-the-middle.test.db"; dnssec-policy common; };' 2>&1 | sed 's/^/I:ns1 /' || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) -# Import skr: in-the-middle -n=$((n + 1)) -echo_i "import ksr to zone 'in-the-middle.test' ($n)" -ret=0 -sleep 2 -$RNDCCMD 10.53.0.1 skr -import in-the-middle.test.skr in-the-middle.test 2>&1 | sed 's/^/I:ns1 /' || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Test that in-the-middle.test is signed and uses the right DNSKEY and RRSIG records. -n=$((n + 1)) -echo_i "test zone 'in-the-middle.test' is correctly signed ($n)" -ret=0 - -set_zone "in-the-middle.test" -set_policy "common" "4" "3600" -set_server "ns1" "10.53.0.1" -# Only ZSKs -key_clear "KEY1" -set_keyrole "KEY1" "zsk" -set_keylifetime "KEY1" "16070400" -set_keyalgorithm "KEY1" "13" "ECDSAP256SHA256" "256" -set_keysigning "KEY1" "no" -set_zonesigning "KEY1" "yes" -set_keystate "KEY1" "GOAL" "omnipresent" -set_keystate "KEY1" "STATE_DNSKEY" "omnipresent" -set_keystate "KEY1" "STATE_ZRRSIG" "omnipresent" - -key_clear "KEY2" -set_keyrole "KEY2" "zsk" -set_keylifetime "KEY2" "16070400" -set_keyalgorithm "KEY2" "13" "ECDSAP256SHA256" "256" -set_keysigning "KEY2" "no" -set_zonesigning "KEY2" "no" -set_keystate "KEY2" "GOAL" "hidden" -set_keystate "KEY2" "STATE_DNSKEY" "hidden" -set_keystate "KEY2" "STATE_ZRRSIG" "hidden" - -key_clear "KEY3" -set_keyrole "KEY3" "zsk" -set_keylifetime "KEY3" "16070400" -set_keyalgorithm "KEY3" "13" "ECDSAP256SHA256" "256" -set_keysigning "KEY3" "no" -set_zonesigning "KEY3" "no" -set_keystate "KEY3" "GOAL" "hidden" -set_keystate "KEY3" "STATE_DNSKEY" "hidden" -set_keystate "KEY3" "STATE_ZRRSIG" "hidden" - -key_clear "KEY4" -set_keyrole "KEY4" "zsk" -set_keylifetime "KEY4" "16070400" -set_keyalgorithm "KEY4" "13" "ECDSAP256SHA256" "256" -set_keysigning "KEY4" "no" -set_zonesigning "KEY4" "no" -set_keystate "KEY4" "GOAL" "hidden" -set_keystate "KEY4" "STATE_DNSKEY" "hidden" -set_keystate "KEY4" "STATE_ZRRSIG" "hidden" - -MAXDEPTH=1 -check_keys -check_dnssecstatus "$SERVER" "$POLICY" "$ZONE" -check_subdomain -dnssec_verify - -# For checking the apex, we need to store the expected KSK metadata. -key_clear "KEY2" -key_clear "KEY3" -key_clear "KEY4" - -set_policy "common" "1" "3600" -set_server "ns1/offline" "10.53.0.1" -set_keyrole "KEY2" "ksk" -set_keylifetime "KEY2" "0" -set_keyalgorithm "KEY2" "13" "ECDSAP256SHA256" "256" -set_keysigning "KEY2" "yes" -set_zonesigning "KEY2" "no" -check_keys "keep" - -DIR="ns1" -set_keystate "KEY2" "STATE_DNSKEY" "omnipresent" -set_keystate "KEY2" "STATE_KRRSIG" "omnipresent" -set_keystate "KEY2" "STATE_DS" "omnipresent" -check_apex - -# Check that key id's match expected keys -n=$((n + 1)) -zsk2=$(cat in-the-middle.test.$DEFAULT_ALGORITHM_NUMBER.zsk2.id) -key1=$(key_get "KEY1" BASEFILE) -echo_i "check that published zsk $zsk2 matches first key $key1 in bundle ($n)" -ret=0 -[ "ns1/$zsk2" = "$key1" ] || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -ksk=$(cat in-the-middle.test.ksk1.id) -key2=$(key_get "KEY2" BASEFILE) -echo_i "check that published ksk $ksk matches ksk $key2 ($n)" -ret=0 -[ "ns1/offline/$ksk" = "$key2" ] || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -echo_i "check that no last bundle warning is logged ($n)" -grep "zone $zone/IN (signed): zone_rekey failure: no available SKR bundle" ns1/named.run && ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Test error conditions -check_rekey_logs_error() { - zone=$1 - inc=$2 - exp=$3 - offset=$4 - - # Key generation - ksr common -K ns1 -i $inc -e $exp keygen $zone >ksr.keygen.out.$n 2>&1 || return 1 - num=$(cat ksr.keygen.out.$n | wc -l) - [ $num -eq 2 ] || return 1 - set_zsk $DEFAULT_ALGORITHM_NUMBER $DEFAULT_BITS 16070400 - ksr_check_keys $zone ns1 $offset || return 1 - # Create request - ksr common -K ns1 -i $inc -e $exp request $zone >ksr.request.out.$n 2>&1 || return 1 - cp ksr.request.out.$n $zone.ksr - # Sign request - ksr common -K ns1/offline -i $inc -e $exp -f $zone.ksr sign $zone >ksr.sign.out.$n 2>&1 || return 1 - cp ksr.sign.out.$n ns1/$zone.skr - # Import skr - $RNDCCMD 10.53.0.1 skr -import $zone.skr $zone 2>&1 | sed 's/^/I:ns1 /' || return 1 - # Test that rekey logs error - wait_for_log 3 "zone $zone/IN (signed): zone_rekey failure: no available SKR bundle" ns1/named.run || return 1 -} - -n=$((n + 1)) -echo_i "check that an SKR that is too old logs error ($n)" -$RNDCCMD 10.53.0.1 addzone 'past.test { type primary; file "past.test.db"; dnssec-policy common; };' 2>&1 | sed 's/^/I:ns1 /' || ret=1 -check_rekey_logs_error "past.test" -2y -1y -63072000 || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -echo_i "check that an SKR that is too new logs error ($n)" -$RNDCCMD 10.53.0.1 addzone 'future.test { type primary; file "future.test.db"; dnssec-policy common; };' 2>&1 | sed 's/^/I:ns1 /' || ret=1 -check_rekey_logs_error "future.test" +1mo +1y 2592000 || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Key generation: csk -n=$((n + 1)) -echo_i "check that 'dnssec-ksr keygen' creates no keys for policy with csk ($n)" -ret=0 -ksr csk -e +2y keygen csk.test >ksr.keygen.out.$n 2>&1 && ret=1 -grep "dnssec-ksr: fatal: policy 'csk' has no zsks" ksr.keygen.out.$n >/dev/null || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Key generation: unlimited -n=$((n + 1)) -echo_i "check that 'dnssec-ksr keygen' creates only one key for zsk with unlimited lifetime ($n)" -ret=0 -ksr unlimited -K ns1 -e +2y keygen unlimited.test >ksr.keygen.out.$n 2>&1 || ret=1 -num=$(cat ksr.keygen.out.$n | wc -l) -[ $num -eq 1 ] || ret=1 -key=$(cat ksr.keygen.out.$n) -grep "; Created:" "ns1/${key}.key" >created.out || ret=1 -created=$(awk '{print $3}' /dev/null || ret=1 -grep "Length: $DEFAULT_BITS" ns1/${key}.state >/dev/null || ret=1 -grep "Lifetime: 0" ns1/${key}.state >/dev/null || ret=1 -grep "KSK: no" ns1/${key}.state >/dev/null || ret=1 -grep "ZSK: yes" ns1/${key}.state >/dev/null || ret=1 -grep "Published: $published" ns1/${key}.state >/dev/null || ret=1 -grep "Active: $active" ns1/${key}.state >/dev/null || ret=1 -grep "Retired:" ns1/${key}.state >/dev/null && ret=1 -grep "Removed:" ns1/${key}.state >/dev/null && ret=1 -cat ns1/${key}.key | grep -v ";.*" >unlimited.test.$DEFAULT_ALGORITHM_NUMBER.zsk1 -echo $key >"unlimited.test.${DEFAULT_ALGORITHM_NUMBER}.zsk1.id" -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Create request: unlimited -n=$((n + 1)) -echo_i "check that 'dnssec-ksr request' creates correct KSR with unlimited zsk ($n)" -ret=0 -ksr unlimited -K ns1 -i $created -e +4y request unlimited.test >ksr.request.out.$n 2>&1 || ret=1 -# Only one bundle: KSK + ZSK -inception=$(cat ns1/$key.state | grep "Generated" | cut -d' ' -f 2-) -echo ";; KeySigningRequest 1.0 $inception" >ksr.request.expect.$n -cat unlimited.test.$DEFAULT_ALGORITHM_NUMBER.zsk1 >>ksr.request.expect.$n -# Footer -grep ";; KeySigningRequest 1.0 generated at" ksr.request.out.$n >footer.$n || ret=1 -cat footer.$n >>ksr.request.expect.$n -diff -w ksr.request.out.$n ksr.request.expect.$n >/dev/null || ret=1 -# Save request for ksr sign operation. -cp ksr.request.expect.$n ksr.request.expect -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Sign request: unlimited -n=$((n + 1)) -echo_i "check that 'dnssec-ksr sign' creates correct SKR with unlimited zsk ($n)" -ret=0 -ksr unlimited -K ns1 -i $created -e +4y -K ns1/offline -f ksr.request.expect sign unlimited.test >ksr.sign.out.$n 2>&1 || ret=1 -start=$(cat ns1/$key.state | grep "Generated" | awk '{print $2}') -end=$(addtime $start 126144000) # four years -check_skr "unlimited.test" "ns1/offline" "ksr.sign.out.$n" $start $end 1 || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Sign request: unlimited (no-cdnskey) -n=$((n + 1)) -echo_i "check that 'dnssec-ksr sign' creates correct SKR with unlimited zsk, no cdnskey ($n)" -ret=0 -ksr no-cdnskey -K ns1 -i $created -e +4y -K ns1/offline -f ksr.request.expect sign unlimited.test >ksr.sign.out.$n 2>&1 || ret=1 -start=$(cat ns1/$key.state | grep "Generated" | awk '{print $2}') -end=$(addtime $start 126144000) # four years -CDNSKEY="no" -CDS_SHA1="yes" -CDS_SHA256="yes" -CDS_SHA384="yes" -check_skr "unlimited.test" "ns1/offline" "ksr.sign.out.$n" $start $end 1 || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Sign request: unlimited (no-cds) -n=$((n + 1)) -echo_i "check that 'dnssec-ksr sign' creates correct SKR with unlimited zsk, no cds ($n)" -ret=0 -ksr no-cds -K ns1 -i $created -e +4y -K ns1/offline -f ksr.request.expect sign unlimited.test >ksr.sign.out.$n 2>&1 || ret=1 -start=$(cat ns1/$key.state | grep "Generated" | awk '{print $2}') -end=$(addtime $start 126144000) # four years -CDNSKEY="yes" -CDS_SHA1="no" -CDS_SHA256="no" -CDS_SHA384="no" -check_skr "unlimited.test" "ns1/offline" "ksr.sign.out.$n" $start $end 1 || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Reset CDS and CDNSKEY to default values -CDNSKEY="yes" -CDS_SHA1="no" -CDS_SHA256="yes" -CDS_SHA384="no" - -# Key generation: two-tone -n=$((n + 1)) -echo_i "check that 'dnssec-ksr keygen' creates keys for different algorithms ($n)" -ret=0 -ksr two-tone -K ns1 -e +1y keygen two-tone.test >ksr.keygen.out.$n 2>&1 || ret=1 -# First algorithm keys have a lifetime of 3 months, so there should be 4 created keys. -alg=$(printf "%03d" "$DEFAULT_ALGORITHM_NUMBER") -num=$(grep "Ktwo-tone.test.+$alg+" ksr.keygen.out.$n | wc -l) -[ $num -eq 4 ] || ret=1 -set_zsk $DEFAULT_ALGORITHM_NUMBER $DEFAULT_BITS 8035200 -ksr_check_keys two-tone.test ns1 0 || ret=1 -cp ksr.keygen.out.$n ksr.keygen.out.expect.$DEFAULT_ALGORITHM_NUMBER -# Second algorithm keys have a lifetime of 5 months, so there should be 3 created keys. -# While only two time bundles of 5 months fit into one year, we need to create an -# extra key for the remainder of the bundle. -alg=$(printf "%03d" "$ALTERNATIVE_ALGORITHM_NUMBER") -num=$(grep "Ktwo-tone.test.+$alg+" ksr.keygen.out.$n | wc -l) -[ $num -eq 3 ] || ret=1 -set_zsk $ALTERNATIVE_ALGORITHM_NUMBER $ALTERNATIVE_BITS 13392000 -ksr_check_keys two-tone.test ns1 0 || ret=1 -cp ksr.keygen.out.$n ksr.keygen.out.expect.$ALTERNATIVE_ALGORITHM_NUMBER -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Create request: two-tone -n=$((n + 1)) -echo_i "check that 'dnssec-ksr request' creates correct KSR with multiple algorithms ($n)" -ret=0 -key=$(cat two-tone.test.$DEFAULT_ALGORITHM_NUMBER.zsk1.id) -grep "; Created:" "ns1/${key}.key" >created.out || ret=1 -created=$(awk '{print $3}' ksr.request.out.$n 2>&1 || ret=1 -# The two-tone policy uses two sets of KSK/ZSK with different algorithms. One -# set uses the default algorithm (denoted as A below), the other is using the -# alternative algorithm (denoted as B). The A-ZSKs roll every three months, -# so in the second bundle there should be a new DNSKEY prepublished, and the -# predecessor is removed in the third bundle. Then, after five months the -# ZSK for the B set is rolled, adding the successor in bundle 4 and removing -# its predecessor in bundle 5. -# -# Bundle 1: KSK-A1, KSK-B1, ZSK-A1, ZSK-B1 -key=$(cat two-tone.test.$DEFAULT_ALGORITHM_NUMBER.zsk1.id) -inception=$(cat ns1/$key.state | grep "Generated" | cut -d' ' -f 2-) -echo ";; KeySigningRequest 1.0 $inception" >ksr.request.expect.$n -cat two-tone.test.$DEFAULT_ALGORITHM_NUMBER.zsk1 >>ksr.request.expect.$n -cat two-tone.test.$ALTERNATIVE_ALGORITHM_NUMBER.zsk1 >>ksr.request.expect.$n -# Bundle 2: KSK-A1, KSK-B1, ZSK-A1 + ZSK-A2, ZSK-B1 -key=$(cat two-tone.test.$DEFAULT_ALGORITHM_NUMBER.zsk2.id) -inception=$(cat ns1/$key.state | grep "Published" | cut -d' ' -f 2-) -echo ";; KeySigningRequest 1.0 $inception" >>ksr.request.expect.$n -print_dnskeys two-tone.test 1 2 $DEFAULT_ALGORITHM_NUMBER ksr.keygen.out.expect.$DEFAULT_ALGORITHM_NUMBER >>ksr.request.expect.$n -cat two-tone.test.$ALTERNATIVE_ALGORITHM_NUMBER.zsk1 >>ksr.request.expect.$n -# Bundle 3: KSK-A1, KSK-B1, ZSK-A2, ZSK-B1 -key=$(cat two-tone.test.$DEFAULT_ALGORITHM_NUMBER.zsk1.id) -inception=$(cat ns1/$key.state | grep "Removed" | cut -d' ' -f 2-) -echo ";; KeySigningRequest 1.0 $inception" >>ksr.request.expect.$n -cat two-tone.test.$DEFAULT_ALGORITHM_NUMBER.zsk2 >>ksr.request.expect.$n -cat two-tone.test.$ALTERNATIVE_ALGORITHM_NUMBER.zsk1 >>ksr.request.expect.$n -# Bundle 4: KSK-A1, KSK-B1, ZSK-A2, ZSK-B1 + ZSK-B2 -key=$(cat two-tone.test.$ALTERNATIVE_ALGORITHM_NUMBER.zsk2.id) -inception=$(cat ns1/$key.state | grep "Published" | cut -d' ' -f 2-) -echo ";; KeySigningRequest 1.0 $inception" >>ksr.request.expect.$n -cat two-tone.test.$DEFAULT_ALGORITHM_NUMBER.zsk2 >>ksr.request.expect.$n -print_dnskeys two-tone.test 1 2 $ALTERNATIVE_ALGORITHM_NUMBER ksr.keygen.out.expect.$ALTERNATIVE_ALGORITHM_NUMBER >>ksr.request.expect.$n -# Bundle 5: KSK-A1, KSK-B1, ZSK-A2, ZSK-B2 -key=$(cat two-tone.test.$ALTERNATIVE_ALGORITHM_NUMBER.zsk1.id) -inception=$(cat ns1/$key.state | grep "Removed" | cut -d' ' -f 2-) -echo ";; KeySigningRequest 1.0 $inception" >>ksr.request.expect.$n -cat two-tone.test.$DEFAULT_ALGORITHM_NUMBER.zsk2 >>ksr.request.expect.$n -cat two-tone.test.$ALTERNATIVE_ALGORITHM_NUMBER.zsk2 >>ksr.request.expect.$n -# Footer -grep ";; KeySigningRequest 1.0 generated at" ksr.request.out.$n >footer.$n || ret=1 -cat footer.$n >>ksr.request.expect.$n -# Check the KSR request against the expected request. -diff -w ksr.request.out.$n ksr.request.expect.$n >/dev/null || ret=1 -# Save request for ksr sign operation. -cp ksr.request.expect.$n ksr.request.expect -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -num_occurrences() { - count="$1" - file="$2" - line="$3" - exclude="$4" - - if [ -z "$exclude" ]; then - lines=$(cat "$file" | while read line; do echo $line; done | grep "$line" | wc -l) - echo_i "$lines occurrences: $1 $2 $3" - else - lines=$(cat "$file" | while read line; do echo $line; done | grep -v "$exclude" | grep "$line" | wc -l) - echo_i "$lines occurrences: $1 $2 $3 (exclude $4)" - fi - - test "$lines" -eq "$count" || return 1 -} - -# Sign request: two-tone -n=$((n + 1)) -echo_i "check that 'dnssec-ksr sign' creates correct SKR with multiple algorithms ($n)" -ret=0 -ksr two-tone -i $created -e +6mo -K ns1/offline -f ksr.request.expect sign two-tone.test >ksr.sign.out.$n 2>&1 || ret=1 -test "$ret" -eq 0 || echo_i "failed" -# Weak testing: -zone="two-tone.test" -# expect 24 headers (including the footer) -num_occurrences 24 ksr.sign.out.$n ";; SignedKeyResponse 1.0" || ret=1 -# expect 23 KSKs and its signatures (for each header one) -num_occurrences 23 ksr.sign.out.$n "DNSKEY 257 3 8" "CDNSKEY" || ret=1 # exclude CDNSKEY lines -test "$ret" -eq 0 || echo_i "2 failed" -num_occurrences 23 ksr.sign.out.$n "DNSKEY 257 3 13" "CDNSKEY" || ret=1 # exclude CDNSKEY lines -test "$ret" -eq 0 || echo_i "3 failed" -num_occurrences 23 ksr.sign.out.$n "RRSIG DNSKEY 8" "CDNSKEY" || ret=1 # exclude CDNSKEY lines -test "$ret" -eq 0 || echo_i "4 failed" -num_occurrences 23 ksr.sign.out.$n "RRSIG DNSKEY 13" "CDNSKEY" || ret=1 # exclude CDNSKEY lines -test "$ret" -eq 0 || echo_i "5 failed" -# ... 23 CDNSKEY records and its signatures -num_occurrences 23 ksr.sign.out.$n "CDNSKEY 257 3 8" || ret=1 -test "$ret" -eq 0 || echo_i "6 failed" -num_occurrences 23 ksr.sign.out.$n "CDNSKEY 257 3 13" || ret=1 -test "$ret" -eq 0 || echo_i "7 failed" -num_occurrences 23 ksr.sign.out.$n "RRSIG CDNSKEY 8" || ret=1 -test "$ret" -eq 0 || echo_i "8 failed" -num_occurrences 23 ksr.sign.out.$n "RRSIG CDNSKEY 13" || ret=1 -test "$ret" -eq 0 || echo_i "9 failed" -# ... 23 CDS records and its signatures -num_occurrences 23 ksr.sign.out.$n "CDS 8 2" || ret=1 -test "$ret" -eq 0 || echo_i "10 failed" -num_occurrences 23 ksr.sign.out.$n "CDS 13 2" || ret=1 -test "$ret" -eq 0 || echo_i "11 failed" -num_occurrences 23 ksr.sign.out.$n "RRSIG CDS 8" || ret=1 -test "$ret" -eq 0 || echo_i "12 failed" -num_occurrences 23 ksr.sign.out.$n "RRSIG CDS 13" || ret=1 -test "$ret" -eq 0 || echo_i "13 failed" -# expect 25 ZSK (two more for double keys during the rollover) -num_occurrences 25 ksr.sign.out.$n "DNSKEY 256 3 8" || ret=1 -test "$ret" -eq 0 || echo_i "14 failed" -num_occurrences 25 ksr.sign.out.$n "DNSKEY 256 3 13" || ret=1 -test "$ret" -eq 0 || echo_i "15 failed" -status=$((status + ret)) - -echo_i "exit status: $status" -[ $status -eq 0 ] || exit 1 diff --git a/bin/tests/system/ksr/tests_ksr.py b/bin/tests/system/ksr/tests_ksr.py new file mode 100644 index 0000000000..efe688d983 --- /dev/null +++ b/bin/tests/system/ksr/tests_ksr.py @@ -0,0 +1,1100 @@ +# 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. + +# pylint: disable=too-many-lines + +import os +import shutil +import time + +from datetime import datetime + +import isctest + +from isctest.kasp import ( + addtime, + cds_equals, + dnskey_equals, + get_keytag, + get_metadata, + get_timing_metadata, +) + + +def between(value, start, end): + if int(value) == 0: + return False + + return int(value) > int(start) and int(value) < int(end) + + +def file_contents_equal(file1, file2): + diff_command = [ + "diff", + "-w", + file1, + file2, + ] + isctest.run.cmd(diff_command) + + +def keygen(zone, policy, keydir, when="now"): + keygen_command = [ + *os.environ.get("KEYGEN").split(), + "-l", + "ns1/named.conf", + "-fK", + "-K", + keydir, + "-k", + policy, + "-P", + when, + "-A", + when, + "-P", + "sync", + when, + zone, + ] + output = isctest.run.cmd(keygen_command, log_stdout=True).stdout.decode("utf-8") + keys = output.split() + return keys + + +def ksr(zone, policy, action, options="", raise_on_exception=True): + ksr_command = [ + *os.environ.get("KSR").split(), + "-l", + "ns1/named.conf", + "-k", + policy, + *options.split(), + action, + zone, + ] + + out = isctest.run.cmd( + ksr_command, log_stdout=True, raise_on_exception=raise_on_exception + ) + return out.stdout.decode("utf-8"), out.stderr.decode("utf-8") + + +# pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-statements +def check_keys(keys, lifetime, alg, size, keydir=None, offset=0, with_state=False): + # Check keys that were created. + inception = 0 + num = 0 + + now = datetime.now().strftime("%Y%m%d%H%M%S") + + for key in keys: + if keydir is not None: + statefile = f"{keydir}/{key}.state" + else: + statefile = f"{key}.state" + + # created: from keyfile plus offset + created = get_timing_metadata(key, "Created", keydir=keydir, offset=offset) + + # active: retired previous key + if num == 0: + active = created + else: + active = retired + + # published: 2h5m (dnskey-ttl + publish-safety + propagation) + published = addtime(active, -7500) + + # retired: zsk-lifetime + if lifetime > 0: + retired = addtime(active, lifetime) + # removed: 10d1h5m + # (ttlsig + retire-safety + sign-delay + propagation) + removed = addtime(retired, 867900) + else: + retired = 0 + removed = 0 + + if between(now, published, retired) or int(retired) == 0: + goal = "omnipresent" + pubdelay = addtime(published, 7500) + signdelay = addtime(active, 867900) + + if between(now, published, pubdelay): + state_dnskey = "rumoured" + else: + state_dnskey = "omnipresent" + + if between(now, active, signdelay): + state_zrrsig = "rumoured" + else: + state_zrrsig = "omnipresent" + else: + goal = "hidden" + state_dnskey = "hidden" + state_zrrsig = "hidden" + + with open(statefile, "r", encoding="utf-8") as file: + metadata = file.read() + assert f"Algorithm: {alg}" in metadata + assert f"Length: {size}" in metadata + assert f"Lifetime: {lifetime}" in metadata + assert "KSK: no" in metadata + assert "ZSK: yes" in metadata + assert f"Published: {published}" in metadata + assert f"Active: {active}" in metadata + + if lifetime > 0: + assert f"Retired: {retired}" in metadata + assert f"Removed: {removed}" in metadata + else: + assert "Retired:" not in metadata + assert "Removed:" not in metadata + + if with_state: + assert f"GoalState: {goal}" in metadata + assert f"DNSKEYState: {state_dnskey}" in metadata + assert f"ZRRSIGState: {state_zrrsig}" in metadata + assert "KRRSIGState:" not in metadata + assert "DSState:" not in metadata + + inception += lifetime + num += 1 + + +def check_keysigningrequest(out, zsks, start, end, keydir=None): + lines = out.split("\n") + line_no = 0 + + inception = start + while int(inception) < int(end): + next_bundle = addtime(end, 1) + # expect bundle header + assert f";; KeySigningRequest 1.0 {inception}" in lines[line_no] + line_no += 1 + # expect zsks + for key in sorted(zsks): + published = get_timing_metadata(key, "Publish", keydir=keydir) + if between(published, inception, next_bundle): + next_bundle = published + + removed = get_timing_metadata( + key, "Delete", keydir=keydir, must_exist=False + ) + if between(removed, inception, next_bundle): + next_bundle = removed + + if int(published) > int(inception): + continue + if int(removed) != 0 and int(inception) >= int(removed): + continue + + # this zsk must be in the ksr + assert dnskey_equals(key, lines[line_no], keydir=keydir) + line_no += 1 + + inception = next_bundle + + # ksr footer + assert ";; KeySigningRequest 1.0 generated at" in lines[line_no] + line_no += 1 + + # trailing empty lines + while line_no < len(lines): + assert lines[line_no] == "" + line_no += 1 + + assert line_no == len(lines) + + +# pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-statements +def check_signedkeyresponse( + out, + zone, + ksks, + zsks, + start, + end, + refresh, + kskdir=None, + zskdir=None, + cdnskey=True, + cds="SHA-256", +): + lines = out.split("\n") + line_no = 0 + next_bundle = addtime(end, 1) + + inception = start + while int(inception) < int(end): + # A single signed key response may consist of: + # ;; SignedKeyResponse (header) + # ;; DNSKEY 257 (one per published key in ksks) + # ;; DNSKEY 256 (one per published key in zsks) + # ;; RRSIG(DNSKEY) (one per active key in ksks) + # ;; CDNSKEY (one per published key in ksks) + # ;; RRSIG(CDNSKEY) (one per active key in ksks) + # ;; CDS (one per published key in ksks) + # ;; RRSIG(CDS) (one per active key in ksks) + + sigstart = addtime(inception, -3600) # clockskew: 1 hour + sigend = addtime(inception, 1209600) # sig-validity: 14 days + next_bundle = addtime(sigend, refresh) + + # ignore empty lines + while line_no < len(lines): + if lines[line_no] == "": + line_no += 1 + else: + break + + # expect bundle header + assert f";; SignedKeyResponse 1.0 {inception}" in lines[line_no] + line_no += 1 + + # expect ksks + for key in sorted(ksks): + published = get_timing_metadata(key, "Publish", keydir=kskdir) + removed = get_timing_metadata( + key, "Delete", keydir=kskdir, must_exist=False + ) + + if int(published) > int(inception): + continue + if int(removed) != 0 and int(inception) >= int(removed): + continue + + # this ksk must be in the ksr + assert dnskey_equals(key, lines[line_no], keydir=kskdir) + line_no += 1 + + # expect zsks + for key in sorted(zsks): + published = get_timing_metadata(key, "Publish", keydir=zskdir) + if between(published, inception, next_bundle): + next_bundle = published + + removed = get_timing_metadata( + key, "Delete", keydir=zskdir, must_exist=False + ) + if between(removed, inception, next_bundle): + next_bundle = removed + + if int(published) > int(inception): + continue + if int(removed) != 0 and int(inception) >= int(removed): + continue + + # this zsk must be in the ksr + assert dnskey_equals(key, lines[line_no], keydir=zskdir) + line_no += 1 + + # expect rrsig(dnskey) + for key in sorted(ksks): + active = get_timing_metadata(key, "Activate", keydir=kskdir) + inactive = get_timing_metadata( + key, "Inactive", keydir=kskdir, must_exist=False + ) + if int(active) > int(inception): + continue + if int(inactive) != 0 and int(inception) >= int(inactive): + continue + + # there must be a signature of this ksk + keytag = get_keytag(key) + alg = get_metadata(key, "Algorithm", keydir=kskdir) + expect = f"{zone}. 3600 IN RRSIG DNSKEY {alg} 2 3600 {sigend} {sigstart} {keytag} {zone}." + rrsig = " ".join(lines[line_no].split()) + assert expect in rrsig + line_no += 1 + + # expect cdnskey + if cdnskey: + for key in sorted(ksks): + published = get_timing_metadata(key, "Publish", keydir=kskdir) + removed = get_timing_metadata( + key, "Delete", keydir=kskdir, must_exist=False + ) + if int(published) > int(inception): + continue + if int(removed) != 0 and int(inception) >= int(removed): + continue + + # the cdnskey of this ksk must be in the ksr + assert dnskey_equals(key, lines[line_no], keydir=kskdir, cdnskey=True) + line_no += 1 + + # expect rrsig(cdnskey) + for key in sorted(ksks): + active = get_timing_metadata(key, "Activate", keydir=kskdir) + inactive = get_timing_metadata( + key, "Inactive", keydir=kskdir, must_exist=False + ) + if int(active) > int(inception): + continue + if int(inactive) != 0 and int(inception) >= int(inactive): + continue + + # there must be a signature of this ksk + keytag = get_keytag(key) + alg = get_metadata(key, "Algorithm", keydir=kskdir) + expect = f"{zone}. 3600 IN RRSIG CDNSKEY {alg} 2 3600 {sigend} {sigstart} {keytag} {zone}." + rrsig = " ".join(lines[line_no].split()) + assert expect in rrsig + line_no += 1 + + # expect cds + if cds != "": + for key in sorted(ksks): + published = get_timing_metadata(key, "Publish", keydir=kskdir) + removed = get_timing_metadata( + key, "Delete", keydir=kskdir, must_exist=False + ) + if int(published) > int(inception): + continue + if int(removed) != 0 and int(inception) >= int(removed): + continue + + # the cds of this ksk must be in the ksr + expected_cds = cds.split(",") + for alg in expected_cds: + assert cds_equals(key, lines[line_no], alg.strip(), keydir=kskdir) + line_no += 1 + + # expect rrsig(cds) + for key in sorted(ksks): + active = get_timing_metadata(key, "Activate", keydir=kskdir) + inactive = get_timing_metadata( + key, "Inactive", keydir=kskdir, must_exist=False + ) + if int(active) > int(inception): + continue + if int(inactive) != 0 and int(inception) >= int(inactive): + continue + + # there must be a signature of this ksk + keytag = get_keytag(key) + alg = get_metadata(key, "Algorithm", keydir=kskdir) + expect = f"{zone}. 3600 IN RRSIG CDS {alg} 2 3600 {sigend} {sigstart} {keytag} {zone}." + rrsig = " ".join(lines[line_no].split()) + assert expect in rrsig + line_no += 1 + + inception = next_bundle + + # skr footer + assert ";; SignedKeyResponse 1.0 generated at" in lines[line_no] + line_no += 1 + + # trailing empty lines + while line_no < len(lines): + assert lines[line_no] == "" + line_no += 1 + + assert line_no == len(lines) + + +def test_ksr_errors(): + # check that 'dnssec-ksr' errors on unknown action + _, err = ksr("common.test", "common", "foobar", raise_on_exception=False) + assert "dnssec-ksr: fatal: unknown command 'foobar'" in err + + # check that 'dnssec-ksr keygen' errors on missing end date + _, err = ksr("common.test", "common", "keygen", raise_on_exception=False) + assert "dnssec-ksr: fatal: keygen requires an end date" in err + + # check that 'dnssec-ksr keygen' errors on zone with csk + _, err = ksr( + "csk.test", "csk", "keygen", options="-K ns1 -e +2y", raise_on_exception=False + ) + assert "dnssec-ksr: fatal: policy 'csk' has no zsks" in err + + # check that 'dnssec-ksr request' errors on missing end date + _, err = ksr("common.test", "common", "request", raise_on_exception=False) + assert "dnssec-ksr: fatal: request requires an end date" in err + + # check that 'dnssec-ksr sign' errors on missing ksr file + _, err = ksr( + "common.test", + "common", + "sign", + options="-K ns1/offline -i now -e +1y", + raise_on_exception=False, + ) + assert "dnssec-ksr: fatal: 'sign' requires a KSR file" in err + + +# pylint: disable=too-many-locals,too-many-statements +def test_ksr_common(servers): + # common test cases (1) + zone = "common.test" + policy = "common" + n = 1 + + # create ksk + ksks = keygen(zone, policy, "ns1/offline") + assert len(ksks) == 1 + + # check that 'dnssec-ksr keygen' pregenerates right amount of keys + out, _ = ksr(zone, policy, "keygen", options="-i now -e +1y") + zsks = out.split() + assert len(zsks) == 2 + + alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") + size = os.environ.get("DEFAULT_BITS") + lifetime = 16070400 + check_keys(zsks, lifetime, alg, size) + + # check that 'dnssec-ksr keygen' pregenerates right amount of keys + # in the given key directory + out, _ = ksr(zone, policy, "keygen", options="-K ns1 -i now -e +1y") + zsks = out.split() + assert len(zsks) == 2 + + alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") + size = os.environ.get("DEFAULT_BITS") + lifetime = 16070400 + check_keys(zsks, lifetime, alg, size, keydir="ns1") + + for key in zsks: + privatefile = f"ns1/{key}.private" + keyfile = f"ns1/{key}.key" + statefile = f"ns1/{key}.state" + shutil.copyfile(privatefile, f"{privatefile}.backup") + shutil.copyfile(keyfile, f"{keyfile}.backup") + shutil.copyfile(statefile, f"{statefile}.backup") + + # check that 'dnssec-ksr request' creates correct ksr + now = get_timing_metadata(zsks[0], "Created", keydir="ns1") + until = addtime(now, 31536000) # 1 year + out, _ = ksr(zone, policy, "request", options=f"-K ns1 -i {now} -e +1y") + + fname = f"{zone}.ksr.{n}" + with open(fname, "w", encoding="utf-8") as file: + file.write(out) + + check_keysigningrequest(out, zsks, now, until, keydir="ns1") + + # check that 'dnssec-ksr sign' creates correct skr + out, _ = ksr( + zone, policy, "sign", options=f"-K ns1/offline -f {fname} -i {now} -e +1y" + ) + + fname = f"{zone}.skr.{n}" + with open(fname, "w", encoding="utf-8") as file: + file.write(out) + + refresh = -432000 # 5 days + check_signedkeyresponse( + out, zone, ksks, zsks, now, until, refresh, kskdir="ns1/offline", zskdir="ns1" + ) + + # common test cases (2) + n = 2 + + # check that 'dnssec-ksr keygen' selects pregenerated keys for + # the same time bundle + out, _ = ksr(zone, policy, "keygen", options=f"-K ns1 -i {now} -e +1y") + selected_zsks = out.split() + assert len(selected_zsks) == 2 + for index, key in enumerate(selected_zsks): + assert zsks[index] == key + file_contents_equal(f"ns1/{key}.private", f"ns1/{key}.private.backup") + file_contents_equal(f"ns1/{key}.key", f"ns1/{key}.key.backup") + file_contents_equal(f"ns1/{key}.state", f"ns1/{key}.state.backup") + + # check that 'dnssec-ksr keygen' generates only necessary keys for + # overlapping time bundle + out, err = ksr(zone, policy, "keygen", options=f"-K ns1 -i {now} -e +2y -v 1") + overlapping_zsks = out.split() + assert len(overlapping_zsks) == 4 + + verbose = err.split() + selected = 0 + generated = 0 + for output in verbose: + if "Selecting" in output: + selected += 1 + if "Generating" in output: + generated += 1 + assert selected == 2 + assert generated == 2 + for index, key in enumerate(overlapping_zsks): + if index < 2: + assert zsks[index] == key + file_contents_equal(f"ns1/{key}.private", f"ns1/{key}.private.backup") + file_contents_equal(f"ns1/{key}.key", f"ns1/{key}.key.backup") + file_contents_equal(f"ns1/{key}.state", f"ns1/{key}.state.backup") + + # run 'dnssec-ksr keygen' again with verbosity 0 + out, _ = ksr(zone, policy, "keygen", options=f"-K ns1 -i {now} -e +2y") + overlapping_zsks2 = out.split() + assert len(overlapping_zsks2) == 4 + check_keys(overlapping_zsks2, lifetime, alg, size, keydir="ns1") + for index, key in enumerate(overlapping_zsks2): + assert overlapping_zsks[index] == key + + # check that 'dnssec-ksr request' creates correct ksr if the + # interval is shorter + out, _ = ksr(zone, policy, "request", options=f"-K ns1 -i {now} -e +1y") + + fname = f"{zone}.ksr.{n}.shorter" + with open(fname, "w", encoding="utf-8") as file: + file.write(out) + + check_keysigningrequest(out, zsks, now, until, keydir="ns1") + + # check that 'dnssec-ksr request' creates correct ksr with new interval + out, _ = ksr(zone, policy, "request", options=f"-K ns1 -i {now} -e +2y") + + fname = f"{zone}.ksr.{n}" + with open(fname, "w", encoding="utf-8") as file: + file.write(out) + + until = addtime(now, 63072000) # 2 years + check_keysigningrequest(out, overlapping_zsks, now, until, keydir="ns1") + + # check that 'dnssec-ksr request' errors if there are not enough keys + _, err = ksr( + zone, + policy, + "request", + options=f"-K ns1 -i {now} -e +3y", + raise_on_exception=False, + ) + error = f"no {zone}/ECDSAP256SHA256 zsk key pair found for bundle" + assert f"dnssec-ksr: fatal: {error}" in err + + # check that 'dnssec-ksr sign' creates correct skr + out, _ = ksr( + zone, policy, "sign", options=f"-K ns1/offline -f {fname} -i {now} -e +2y" + ) + + fname = f"{zone}.skr.{n}" + with open(fname, "w", encoding="utf-8") as file: + file.write(out) + + refresh = -432000 # 5 days + check_signedkeyresponse( + out, + zone, + ksks, + overlapping_zsks, + now, + until, + refresh, + kskdir="ns1/offline", + zskdir="ns1", + ) + + # add zone + ns1 = servers["ns1"] + ns1.rndc( + f"addzone {zone} " + + "{ type primary; file " + + f'"{zone}.db"; dnssec-policy {policy}; ' + + "};", + log=False, + ) + + # import skr + shutil.copyfile(fname, f"ns1/{fname}") + ns1.rndc(f"skr -import {fname} {zone}", log=False) + + # test zone is correctly signed + # - check rndc dnssec -status output + isctest.kasp.check_dnssecstatus(ns1, zone, overlapping_zsks, policy=policy) + # - zone is signed + isctest.kasp.zone_is_signed(ns1, zone) + # - dnssec_verify + isctest.kasp.dnssec_verify(ns1, zone) + # - check keys + check_keys(overlapping_zsks, lifetime, alg, size, keydir="ns1", with_state=True) + # - check apex + isctest.kasp.check_apex( + ns1, zone, ksks, overlapping_zsks, kskdir="ns1/offline", zskdir="ns1" + ) + # - check subdomain + isctest.kasp.check_subdomain( + ns1, zone, ksks, overlapping_zsks, kskdir="ns1/offline", zskdir="ns1" + ) + + +# pylint: disable=too-many-locals +def test_ksr_lastbundle(servers): + zone = "last-bundle.test" + policy = "common" + n = 1 + + # create ksk + now = datetime.now().strftime("%Y%m%d%H%M%S") + offset = -31536000 + when = addtime(now, offset) + when = addtime(when, -86400) + ksks = keygen(zone, policy, "ns1/offline", when=when) + assert len(ksks) == 1 + + # check that 'dnssec-ksr keygen' pregenerates right amount of keys + out, _ = ksr(zone, policy, "keygen", options="-K ns1 -i -1y -e +1d") + zsks = out.split() + assert len(zsks) == 2 + + alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") + size = os.environ.get("DEFAULT_BITS") + lifetime = 16070400 + check_keys(zsks, lifetime, alg, size, keydir="ns1", offset=offset) + + # check that 'dnssec-ksr request' creates correct ksr + then = get_timing_metadata(zsks[0], "Created", keydir="ns1") + then = addtime(then, offset) + until = addtime(then, 31622400) # 1 year, 1 day + out, _ = ksr(zone, policy, "request", options=f"-K ns1 -i {then} -e +1d") + + fname = f"{zone}.ksr.{n}" + with open(fname, "w", encoding="utf-8") as file: + file.write(out) + + check_keysigningrequest(out, zsks, then, until, keydir="ns1") + + # check that 'dnssec-ksr sign' creates correct skr + out, _ = ksr( + zone, policy, "sign", options=f"-K ns1/offline -f {fname} -i {then} -e +1d" + ) + + fname = f"{zone}.skr.{n}" + with open(fname, "w", encoding="utf-8") as file: + file.write(out) + + refresh = -432000 # 5 days + check_signedkeyresponse( + out, zone, ksks, zsks, then, until, refresh, kskdir="ns1/offline", zskdir="ns1" + ) + + # add zone + ns1 = servers["ns1"] + ns1.rndc( + f"addzone {zone} " + + "{ type primary; file " + + f'"{zone}.db"; dnssec-policy {policy}; ' + + "};", + log=False, + ) + + # import skr + shutil.copyfile(fname, f"ns1/{fname}") + ns1.rndc(f"skr -import {fname} {zone}", log=False) + + # test zone is correctly signed + # - check rndc dnssec -status output + isctest.kasp.check_dnssecstatus(ns1, zone, zsks, policy=policy) + # - zone is signed + isctest.kasp.zone_is_signed(ns1, zone) + # - dnssec_verify + isctest.kasp.dnssec_verify(ns1, zone) + # - check keys + check_keys(zsks, lifetime, alg, size, keydir="ns1", offset=offset, with_state=True) + # - check apex + isctest.kasp.check_apex(ns1, zone, ksks, zsks, kskdir="ns1/offline", zskdir="ns1") + # - check subdomain + isctest.kasp.check_subdomain( + ns1, zone, ksks, zsks, kskdir="ns1/offline", zskdir="ns1" + ) + + # check that last bundle warning is logged + warning = "last bundle in skr, please import new skr file" + assert f"zone {zone}/IN (signed): zone_rekey: {warning}" in ns1.log + + +# pylint: disable=too-many-locals +def test_ksr_inthemiddle(servers): + zone = "in-the-middle.test" + policy = "common" + n = 1 + + # create ksk + now = datetime.now().strftime("%Y%m%d%H%M%S") + offset = -31536000 + when = addtime(now, offset) + when = addtime(when, -86400) + ksks = keygen(zone, policy, "ns1/offline", when=when) + assert len(ksks) == 1 + + # check that 'dnssec-ksr keygen' pregenerates right amount of keys + out, _ = ksr(zone, policy, "keygen", options="-K ns1 -i -1y -e +1y") + zsks = out.split() + assert len(zsks) == 4 + + alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") + size = os.environ.get("DEFAULT_BITS") + lifetime = 16070400 + check_keys(zsks, lifetime, alg, size, keydir="ns1", offset=offset) + + # check that 'dnssec-ksr request' creates correct ksr + then = get_timing_metadata(zsks[0], "Created", keydir="ns1") + then = addtime(then, offset) + until = addtime(then, 63072000) # 2 years + out, _ = ksr(zone, policy, "request", options=f"-K ns1 -i {then} -e +1y") + + fname = f"{zone}.ksr.{n}" + with open(fname, "w", encoding="utf-8") as file: + file.write(out) + + check_keysigningrequest(out, zsks, then, until, keydir="ns1") + + # check that 'dnssec-ksr sign' creates correct skr + out, _ = ksr( + zone, policy, "sign", options=f"-K ns1/offline -f {fname} -i {then} -e +1y" + ) + + fname = f"{zone}.skr.{n}" + with open(fname, "w", encoding="utf-8") as file: + file.write(out) + + refresh = -432000 # 5 days + check_signedkeyresponse( + out, zone, ksks, zsks, then, until, refresh, kskdir="ns1/offline", zskdir="ns1" + ) + + # add zone + ns1 = servers["ns1"] + ns1.rndc( + f"addzone {zone} " + + "{ type primary; file " + + f'"{zone}.db"; dnssec-policy {policy}; ' + + "};", + log=False, + ) + + # import skr + shutil.copyfile(fname, f"ns1/{fname}") + ns1.rndc(f"skr -import {fname} {zone}", log=False) + + # test zone is correctly signed + # - check rndc dnssec -status output + isctest.kasp.check_dnssecstatus(ns1, zone, zsks, policy=policy) + # - zone is signed + isctest.kasp.zone_is_signed(ns1, zone) + # - dnssec_verify + isctest.kasp.dnssec_verify(ns1, zone) + # - check keys + check_keys(zsks, lifetime, alg, size, keydir="ns1", offset=offset, with_state=True) + # - check apex + isctest.kasp.check_apex(ns1, zone, ksks, zsks, kskdir="ns1/offline", zskdir="ns1") + # - check subdomain + isctest.kasp.check_subdomain( + ns1, zone, ksks, zsks, kskdir="ns1/offline", zskdir="ns1" + ) + + # check that no last bundle warning is logged + warning = "last bundle in skr, please import new skr file" + assert f"zone {zone}/IN (signed): zone_rekey: {warning}" not in ns1.log + + +# pylint: disable=too-many-locals +def check_ksr_rekey_logs_error(server, zone, policy, offset, end): + n = 1 + + # create ksk + now = datetime.now().strftime("%Y%m%d%H%M%S") + then = addtime(now, offset) + until = addtime(now, end) + ksks = keygen(zone, policy, "ns1/offline", when=then) + assert len(ksks) == 1 + + # key generation + out, _ = ksr(zone, policy, "keygen", options=f"-K ns1 -i {then} -e {until}") + zsks = out.split() + assert len(zsks) == 2 + + # create request + now = get_timing_metadata(zsks[0], "Created", keydir="ns1") + then = addtime(now, offset) + until = addtime(now, end) + out, _ = ksr(zone, policy, "request", options=f"-K ns1 -i {then} -e {until}") + + fname = f"{zone}.ksr.{n}" + with open(fname, "w", encoding="utf-8") as file: + file.write(out) + + # sign request + out, _ = ksr( + zone, policy, "sign", options=f"-K ns1/offline -f {fname} -i {then} -e {until}" + ) + + fname = f"{zone}.skr.{n}" + with open(fname, "w", encoding="utf-8") as file: + file.write(out) + + # add zone + server.rndc( + f"addzone {zone} " + + "{ type primary; file " + + f'"{zone}.db"; dnssec-policy {policy}; ' + + "};", + log=False, + ) + + # import skr + shutil.copyfile(fname, f"ns1/{fname}") + server.rndc(f"skr -import {fname} {zone}", log=False) + + # test that rekey logs error + time_remaining = 10 + warning = "no available SKR bundle" + line = f"zone {zone}/IN (signed): zone_rekey failure: {warning}" + while time_remaining > 0: + if line not in server.log: + time_remaining -= 1 + time.sleep(1) + else: + break + assert line in server.log + + +def test_ksr_rekey_logs_error(servers): + # check that an SKR that is too old logs error + check_ksr_rekey_logs_error( + servers["ns1"], "past.test", "common", -63072000, -31536000 + ) + # check that an SKR that is too new logs error + check_ksr_rekey_logs_error( + servers["ns1"], "future.test", "common", 2592000, 31536000 + ) + + +# pylint: disable=too-many-locals +def test_ksr_unlimited(servers): + zone = "unlimited.test" + policy = "unlimited" + n = 1 + + # create ksk + ksks = keygen(zone, policy, "ns1/offline") + assert len(ksks) == 1 + + # check that 'dnssec-ksr keygen' pregenerates right amount of keys + out, _ = ksr(zone, policy, "keygen", options="-K ns1 -i now -e +2y") + zsks = out.split() + assert len(zsks) == 1 + + alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") + size = os.environ.get("DEFAULT_BITS") + lifetime = 0 + check_keys(zsks, lifetime, alg, size, keydir="ns1") + + # check that 'dnssec-ksr request' creates correct ksr + now = get_timing_metadata(zsks[0], "Created", keydir="ns1") + until = addtime(now, 4 * 31536000) # 4 years + out, _ = ksr(zone, policy, "request", options=f"-K ns1 -i {now} -e +4y") + + fname = f"{zone}.ksr.{n}" + with open(fname, "w", encoding="utf-8") as file: + file.write(out) + + check_keysigningrequest(out, zsks, now, until, keydir="ns1") + + # check that 'dnssec-ksr sign' creates correct skr without cdnskey + out, _ = ksr( + zone, "no-cdnskey", "sign", options=f"-K ns1/offline -f {fname} -i {now} -e +4y" + ) + + skrfile = f"{zone}.no-cdnskey.skr.{n}" + with open(skrfile, "w", encoding="utf-8") as file: + file.write(out) + + refresh = -432000 # 5 days + check_signedkeyresponse( + out, + zone, + ksks, + zsks, + now, + until, + refresh, + kskdir="ns1/offline", + zskdir="ns1", + cdnskey=False, + cds="SHA-1, SHA-256, SHA-384", + ) + + # check that 'dnssec-ksr sign' creates correct skr without cds + out, _ = ksr( + zone, "no-cds", "sign", options=f"-K ns1/offline -f {fname} -i {now} -e +4y" + ) + + skrfile = f"{zone}.no-cds.skr.{n}" + with open(skrfile, "w", encoding="utf-8") as file: + file.write(out) + + refresh = -432000 # 5 days + check_signedkeyresponse( + out, + zone, + ksks, + zsks, + now, + until, + refresh, + kskdir="ns1/offline", + zskdir="ns1", + cds="", + ) + + # check that 'dnssec-ksr sign' creates correct skr + out, _ = ksr( + zone, policy, "sign", options=f"-K ns1/offline -f {fname} -i {now} -e +4y" + ) + + skrfile = f"{zone}.{policy}.skr.{n}" + with open(skrfile, "w", encoding="utf-8") as file: + file.write(out) + + refresh = -432000 # 5 days + check_signedkeyresponse( + out, zone, ksks, zsks, now, until, refresh, kskdir="ns1/offline", zskdir="ns1" + ) + + # add zone + ns1 = servers["ns1"] + ns1.rndc( + f"addzone {zone} " + + "{ type primary; file " + + f'"{zone}.db"; dnssec-policy {policy}; ' + + "};", + log=False, + ) + + # import skr + shutil.copyfile(skrfile, f"ns1/{skrfile}") + ns1.rndc(f"skr -import {skrfile} {zone}", log=False) + + # test zone is correctly signed + # - check rndc dnssec -status output + isctest.kasp.check_dnssecstatus(ns1, zone, zsks, policy=policy) + # - zone is signed + isctest.kasp.zone_is_signed(ns1, zone) + # - dnssec_verify + isctest.kasp.dnssec_verify(ns1, zone) + # - check keys + check_keys(zsks, lifetime, alg, size, keydir="ns1", with_state=True) + # - check apex + isctest.kasp.check_apex(ns1, zone, ksks, zsks, kskdir="ns1/offline", zskdir="ns1") + # - check subdomain + isctest.kasp.check_subdomain( + ns1, zone, ksks, zsks, kskdir="ns1/offline", zskdir="ns1" + ) + + +# pylint: disable=too-many-locals +def test_ksr_twotone(servers): + zone = "two-tone.test" + policy = "two-tone" + n = 1 + + # create ksk + ksks = keygen(zone, policy, "ns1/offline") + assert len(ksks) == 2 + + # check that 'dnssec-ksr keygen' pregenerates right amount of keys + out, _ = ksr(zone, policy, "keygen", options="-K ns1 -i now -e +1y") + zsks = out.split() + # First algorithm keys have a lifetime of 3 months, so there should + # be 4 created keys. Second algorithm keys have a lifetime of 5 + # months, so there should be 3 created keys. While only two time + # bundles of 5 months fit into one year, we need to create an extra + # key for the remainder of the bundle. So 7 in total. + assert len(zsks) == 7 + + zsks_defalg = [] + zsks_altalg = [] + for zsk in zsks: + alg = get_metadata(zsk, "Algorithm", keydir="ns1") + if alg == os.environ.get("DEFAULT_ALGORITHM_NUMBER"): + zsks_defalg.append(zsk) + elif alg == os.environ.get("ALTERNATIVE_ALGORITHM_NUMBER"): + zsks_altalg.append(zsk) + + assert len(zsks_defalg) == 4 + assert len(zsks_altalg) == 3 + + alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") + size = os.environ.get("DEFAULT_BITS") + lifetime = 8035200 # 3 months + check_keys(zsks_defalg, lifetime, alg, size, keydir="ns1") + + alg = os.environ.get("ALTERNATIVE_ALGORITHM_NUMBER") + size = os.environ.get("ALTERNATIVE_BITS") + lifetime = 13392000 # 5 months + check_keys(zsks_altalg, lifetime, alg, size, keydir="ns1") + + # check that 'dnssec-ksr request' creates correct ksr + now = get_timing_metadata(zsks[0], "Created", keydir="ns1") + until = addtime(now, 31536000) # 1 year + out, _ = ksr(zone, policy, "request", options=f"-K ns1 -i {now} -e +1y") + + fname = f"{zone}.ksr.{n}" + with open(fname, "w", encoding="utf-8") as file: + file.write(out) + + check_keysigningrequest(out, zsks, now, until, keydir="ns1") + + # check that 'dnssec-ksr sign' creates correct skr + out, _ = ksr( + zone, policy, "sign", options=f"-K ns1/offline -f {fname} -i {now} -e +1y" + ) + + skrfile = f"{zone}.skr.{n}" + with open(skrfile, "w", encoding="utf-8") as file: + file.write(out) + + refresh = -432000 # 5 days + check_signedkeyresponse( + out, zone, ksks, zsks, now, until, refresh, kskdir="ns1/offline", zskdir="ns1" + ) + + # add zone + ns1 = servers["ns1"] + ns1.rndc( + f"addzone {zone} " + + "{ type primary; file " + + f'"{zone}.db"; dnssec-policy {policy}; ' + + "};", + log=False, + ) + + # import skr + shutil.copyfile(skrfile, f"ns1/{skrfile}") + ns1.rndc(f"skr -import {skrfile} {zone}", log=False) + + # test zone is correctly signed + # - check rndc dnssec -status output + isctest.kasp.check_dnssecstatus(ns1, zone, zsks, policy=policy) + # - zone is signed + isctest.kasp.zone_is_signed(ns1, zone) + # - dnssec_verify + isctest.kasp.dnssec_verify(ns1, zone) + # - check keys + alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") + size = os.environ.get("DEFAULT_BITS") + lifetime = 8035200 # 3 months + check_keys(zsks_defalg, lifetime, alg, size, keydir="ns1", with_state=True) + + alg = os.environ.get("ALTERNATIVE_ALGORITHM_NUMBER") + size = os.environ.get("ALTERNATIVE_BITS") + lifetime = 13392000 # 5 months + check_keys(zsks_altalg, lifetime, alg, size, keydir="ns1", with_state=True) + # - check apex + isctest.kasp.check_apex(ns1, zone, ksks, zsks, kskdir="ns1/offline", zskdir="ns1") + # - check subdomain + isctest.kasp.check_subdomain( + ns1, zone, ksks, zsks, kskdir="ns1/offline", zskdir="ns1" + ) diff --git a/bin/tests/system/ksr/tests_sh_ksr.py b/bin/tests/system/ksr/tests_sh_ksr.py deleted file mode 100644 index 56f07ce094..0000000000 --- a/bin/tests/system/ksr/tests_sh_ksr.py +++ /dev/null @@ -1,14 +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. - - -def test_ksr(run_tests_sh): - run_tests_sh() From f5adeb680072d3f2ca8b7f3646b29609389c9483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Mon, 7 Oct 2024 18:08:02 +0200 Subject: [PATCH 04/12] Use convenience wrappers for kasp key operations (cherry picked from commit 2b0a8fcfb5084b23477f1c66b9f32445422a4461) --- bin/tests/system/isctest/kasp.py | 404 +++++++++++++------------ bin/tests/system/ksr/tests_ksr.py | 474 ++++++++++++++---------------- 2 files changed, 431 insertions(+), 447 deletions(-) diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py index 685250f291..6fbc57b3b5 100644 --- a/bin/tests/system/isctest/kasp.py +++ b/bin/tests/system/isctest/kasp.py @@ -9,8 +9,12 @@ # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. +from functools import total_ordering import os +from pathlib import Path +import re import time +from typing import Optional, Union from datetime import datetime from datetime import timedelta @@ -41,152 +45,191 @@ def _query(server, qname, qtype, outfile=None): return response -def addtime(value, plus): - # Get timing metadata from a value plus additional time. - # Convert "%Y%m%d%H%M%S" format to epoch seconds. - # Then, add the additional time (can be negative). - now = datetime.strptime(value, "%Y%m%d%H%M%S") - delta = timedelta(seconds=plus) - then = now + delta - return then.strftime("%Y%m%d%H%M%S") +@total_ordering +class KeyTimingMetadata: + """ + Represent a single timing information for a key. + + These objects can be easily compared, support addition and subtraction of + timedelta objects or integers(value in seconds). A lack of timing metadata + in the key (value 0) should be represented with None rather than an + instance of this object. + """ + + FORMAT = "%Y%m%d%H%M%S" + + def __init__(self, timestamp: str): + if int(timestamp) <= 0: + raise ValueError(f'invalid timing metadata value: "{timestamp}"') + self.value = datetime.strptime(timestamp, self.FORMAT) + + def __repr__(self): + return self.value.strftime(self.FORMAT) + + def __str__(self) -> str: + return self.value.strftime(self.FORMAT) + + def __add__(self, other: Union[timedelta, int]): + if isinstance(other, int): + other = timedelta(seconds=other) + result = KeyTimingMetadata.__new__(KeyTimingMetadata) + result.value = self.value + other + return result + + def __sub__(self, other: Union[timedelta, int]): + if isinstance(other, int): + other = timedelta(seconds=other) + result = KeyTimingMetadata.__new__(KeyTimingMetadata) + result.value = self.value - other + return result + + def __iadd__(self, other: Union[timedelta, int]): + if isinstance(other, int): + other = timedelta(seconds=other) + self.value += other + + def __isub__(self, other: Union[timedelta, int]): + if isinstance(other, int): + other = timedelta(seconds=other) + self.value -= other + + def __lt__(self, other: "KeyTimingMetadata"): + return self.value < other.value + + def __eq__(self, other: object): + return isinstance(other, KeyTimingMetadata) and self.value == other.value + + @staticmethod + def now() -> "KeyTimingMetadata": + result = KeyTimingMetadata.__new__(KeyTimingMetadata) + result.value = datetime.now() + return result -def get_timing_metadata(key, metadata, keydir=None, offset=0, must_exist=True): - value = "0" +@total_ordering +class Key: + """ + Represent a key from a keyfile. - if keydir is not None: - keyfile = "{}/{}.key".format(keydir, key) - else: - keyfile = "{}.key".format(key) + 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. + """ - with open(keyfile, "r", encoding="utf-8") as file: - for line in file: - if "; {}".format(metadata) in line: - value = line.split()[2] - break + def __init__(self, name: str, keydir: Optional[Union[str, Path]] = None): + self.name = name + if keydir is None: + self.keydir = Path() + else: + self.keydir = Path(keydir) + self.path = str(self.keydir / name) + self.keyfile = f"{self.path}.key" + self.statefile = f"{self.path}.state" + self.tag = int(self.name[-5:]) - if must_exist: - assert int(value) > 0 + def get_timing( + self, metadata: str, must_exist: bool = True + ) -> Optional[KeyTimingMetadata]: + regex = rf";\s+{metadata}:\s+(\d+).*" + with open(self.keyfile, "r", encoding="utf-8") as file: + for line in file: + match = re.match(regex, line) + if match is not None: + try: + return KeyTimingMetadata(match.group(1)) + except ValueError: + break + if must_exist: + raise ValueError( + f'timing metadata "{metadata}" for key "{self.name}" invalid' + ) + return None - if int(value) > 0: - return addtime(value, offset) + def get_metadata(self, metadata: str, must_exist=True) -> str: + value = "undefined" + regex = rf"{metadata}:\s+(.*)" + with open(self.statefile, "r", encoding="utf-8") as file: + for line in file: + match = re.match(regex, line) + if match is not None: + value = match.group(1) + break + if must_exist and value == "undefined": + raise ValueError( + 'state metadata "{metadata}" for key "{self.name}" undefined' + ) + return value - return "0" + def is_ksk(self) -> bool: + return self.get_metadata("KSK") == "yes" + def is_zsk(self) -> bool: + return self.get_metadata("ZSK") == "yes" -def get_metadata(key, metadata, keydir=None, must_exist=True): - if keydir is not None: - statefile = "{}/{}.state".format(keydir, key) - else: - statefile = "{}.state".format(key) + def dnskey_equals(self, value, cdnskey=False): + dnskey = value.split() - value = "undefined" - with open(statefile, "r", encoding="utf-8") as file: - for line in file: - if f"{metadata}: " in line: - value = line.split()[1] - break + if cdnskey: + # fourth element is the rrtype + assert dnskey[3] == "CDNSKEY" + dnskey[3] = "DNSKEY" - if must_exist: - assert value != "undefined" + dnskey_fromfile = [] + rdata = " ".join(dnskey[:7]) - return value + with open(self.keyfile, "r", encoding="utf-8") as file: + for line in file: + if f"{rdata}" in line: + dnskey_fromfile = line.split() + pubkey_fromfile = "".join(dnskey_fromfile[7:]) + pubkey_fromwire = "".join(dnskey[7:]) -def get_keystate(key, metadata, keydir=None, must_exist=True): + return pubkey_fromfile == pubkey_fromwire - return get_metadata(key, metadata, keydir, must_exist) + def cds_equals(self, value, alg): + cds = value.split() + dsfromkey_command = [ + os.environ.get("DSFROMKEY"), + "-T", + "3600", + "-a", + alg, + "-C", + "-w", + str(self.keyfile), + ] -def get_keytag(key): - return int(key[-5:]) + out = isctest.run.cmd(dsfromkey_command, log_stdout=True) + dsfromkey = out.stdout.decode("utf-8").split() + rdata_fromfile = " ".join(dsfromkey[:7]) + rdata_fromwire = " ".join(cds[:7]) + if rdata_fromfile != rdata_fromwire: + isctest.log.debug( + f"CDS RDATA MISMATCH: {rdata_fromfile} - {rdata_fromwire}" + ) + return False -def get_keyrole(key, keydir=None): - ksk = "no" - zsk = "no" + digest_fromfile = "".join(dsfromkey[7:]).lower() + digest_fromwire = "".join(cds[7:]).lower() + if digest_fromfile != digest_fromwire: + isctest.log.debug( + f"CDS DIGEST MISMATCH: {digest_fromfile} - {digest_fromwire}" + ) + return False - if keydir is not None: - statefile = "{}/{}.state".format(keydir, key) - else: - statefile = "{}.state".format(key) + return digest_fromfile == digest_fromwire - with open(statefile, "r", encoding="utf-8") as file: - for line in file: - if "KSK: " in line: - ksk = line.split()[1] - if "ZSK: " in line: - zsk = line.split()[1] + def __lt__(self, other: "Key"): + return self.name < other.name - return ksk == "yes", zsk == "yes" + def __eq__(self, other: object): + return isinstance(other, Key) and self.path == other.path - -def dnskey_equals(key, value, keydir=None, cdnskey=False): - if keydir is not None: - keyfile = f"{keydir}/{key}.key" - else: - keyfile = f"{key}.key" - - dnskey = value.split() - - if cdnskey: - # fourth element is the rrtype - assert dnskey[3] == "CDNSKEY" - dnskey[3] = "DNSKEY" - - dnskey_fromfile = [] - rdata = " ".join(dnskey[:7]) - - with open(keyfile, "r", encoding="utf-8") as file: - for line in file: - if f"{rdata}" in line: - dnskey_fromfile = line.split() - - pubkey_fromfile = "".join(dnskey_fromfile[7:]) - pubkey_fromwire = "".join(dnskey[7:]) - - return pubkey_fromfile == pubkey_fromwire - - -def cds_equals(key, value, alg, keydir=None): - if keydir is not None: - keyfile = f"{keydir}/{key}.key" - else: - keyfile = f"{key}.key" - - cds = value.split() - - dsfromkey_command = [ - *os.environ.get("DSFROMKEY").split(), - "-T", - "3600", - "-a", - alg, - "-C", - "-w", - keyfile, - ] - - out = isctest.run.cmd(dsfromkey_command, log_stdout=True) - dsfromkey = out.stdout.decode("utf-8").split() - index = 6 - while index < len(cds): - dsfromkey[index] = dsfromkey[index].lower() - index += 1 - - rdata_fromfile = " ".join(dsfromkey[:7]) - rdata_fromwire = " ".join(cds[:7]) - if rdata_fromfile != rdata_fromwire: - isctest.log.debug(f"CDS RDATA MISMATCH: {rdata_fromfile} - {rdata_fromwire}") - return False - - digest_fromfile = "".join(cds[7:]) - digest_fromwire = "".join(cds[7:]) - if digest_fromfile != digest_fromwire: - isctest.log.debug(f"CDS DIGEST MISMATCH: {digest_fromfile} - {digest_fromwire}") - return False - - return digest_fromfile == digest_fromwire + def __repr__(self): + return self.path def zone_is_signed(server, zone): @@ -278,13 +321,11 @@ def check_dnssecstatus(server, zone, keys, policy=None, view=None): assert "dnssec-policy: {}".format(policy) in response for key in keys: - keytag = get_keytag(key) - assert "key: {}".format(keytag) in response + assert "key: {}".format(key.tag) in response -# pylint: disable=too-many-locals,too-many-branches -def _check_signatures(signatures, covers, fqdn, keys, keydir=None): - now = datetime.now().strftime("%Y%m%d%H%M%S") +def _check_signatures(signatures, covers, fqdn, keys): + now = KeyTimingMetadata.now() numsigs = 0 zrrsig = True if covers in [dns.rdatatype.DNSKEY, dns.rdatatype.CDNSKEY, dns.rdatatype.CDS]: @@ -292,51 +333,48 @@ def _check_signatures(signatures, covers, fqdn, keys, keydir=None): krrsig = not zrrsig for key in keys: - keytag = get_keytag(key) - ksk, zsk = get_keyrole(key, keydir=keydir) - activate = get_timing_metadata(key, "Activate", keydir=keydir) - inactive = get_timing_metadata(key, "Inactive", keydir=keydir, must_exist=False) + activate = key.get_timing("Activate") + inactive = key.get_timing("Inactive", must_exist=False) - active = int(now) >= int(activate) - retired = int(inactive) != 0 and int(inactive) <= int(now) + active = now >= activate + retired = inactive is not None and inactive <= now signing = active and not retired if not signing: for rrsig in signatures: - assert f"{keytag} {fqdn}" not in rrsig + assert f"{key.tag} {fqdn}" not in rrsig continue - if zrrsig and zsk: + if zrrsig and key.is_zsk(): has_rrsig = False for rrsig in signatures: - if f"{keytag} {fqdn}" in rrsig: + if f"{key.tag} {fqdn}" in rrsig: has_rrsig = True break assert has_rrsig numsigs += 1 - if zrrsig and not zsk: + if zrrsig and not key.is_zsk(): for rrsig in signatures: - assert f"{keytag} {fqdn}" not in rrsig + assert f"{key.tag} {fqdn}" not in rrsig - if krrsig and ksk: + if krrsig and key.is_ksk(): has_rrsig = False for rrsig in signatures: - if f"{keytag} {fqdn}" in rrsig: + if f"{key.tag} {fqdn}" in rrsig: has_rrsig = True break assert has_rrsig numsigs += 1 - if krrsig and not ksk: + if krrsig and not key.is_ksk(): for rrsig in signatures: - assert f"{keytag} {fqdn}" not in rrsig + assert f"{key.tag} {fqdn}" not in rrsig return numsigs -# pylint: disable=too-many-arguments -def check_signatures(rrset, covers, fqdn, ksks, zsks, kskdir=None, zskdir=None): +def check_signatures(rrset, covers, fqdn, ksks, zsks): # Check if signatures with covering type are signed with the right keys. # The right keys are the ones that expect a signature and have the # correct role. @@ -350,14 +388,14 @@ def check_signatures(rrset, covers, fqdn, ksks, zsks, kskdir=None, zskdir=None): rrsig = f"{rr.name} {rr.ttl} {rdclass} {rdtype} {rdata}" signatures.append(rrsig) - numsigs += _check_signatures(signatures, covers, fqdn, ksks, keydir=kskdir) - numsigs += _check_signatures(signatures, covers, fqdn, zsks, keydir=zskdir) + numsigs += _check_signatures(signatures, covers, fqdn, ksks) + numsigs += _check_signatures(signatures, covers, fqdn, zsks) assert numsigs == len(signatures) -def _check_dnskeys(dnskeys, keys, keydir=None, cdnskey=False): - now = datetime.now().strftime("%Y%m%d%H%M%S") +def _check_dnskeys(dnskeys, keys, cdnskey=False): + now = KeyTimingMetadata.now() numkeys = 0 publish_md = "Publish" @@ -367,19 +405,19 @@ def _check_dnskeys(dnskeys, keys, keydir=None, cdnskey=False): delete_md = f"Sync{delete_md}" for key in keys: - publish = get_timing_metadata(key, publish_md, keydir=keydir) - delete = get_timing_metadata(key, delete_md, keydir=keydir, must_exist=False) - published = int(now) >= int(publish) - removed = int(delete) != 0 and int(delete) <= int(now) + publish = key.get_timing(publish_md) + delete = key.get_timing(delete_md, must_exist=False) + published = now >= publish + removed = delete is not None and delete <= now if not published or removed: for dnskey in dnskeys: - assert not dnskey_equals(key, dnskey, keydir=keydir, cdnskey=cdnskey) + assert not key.dnskey_equals(dnskey, cdnskey=cdnskey) continue has_dnskey = False for dnskey in dnskeys: - if dnskey_equals(key, dnskey, keydir=keydir, cdnskey=cdnskey): + if key.dnskey_equals(dnskey, cdnskey=cdnskey): has_dnskey = True break @@ -389,8 +427,7 @@ def _check_dnskeys(dnskeys, keys, keydir=None, cdnskey=False): return numkeys -# pylint: disable=too-many-arguments -def check_dnskeys(rrset, ksks, zsks, kskdir=None, zskdir=None, cdnskey=False): +def check_dnskeys(rrset, ksks, zsks, cdnskey=False): # Check if the correct DNSKEY records are published. If the current time # is between the timing metadata 'publish' and 'delete', the key must have # a DNSKEY record published. If 'cdnskey' is True, check against CDNSKEY @@ -405,20 +442,20 @@ def check_dnskeys(rrset, ksks, zsks, kskdir=None, zskdir=None, cdnskey=False): dnskey = f"{rr.name} {rr.ttl} {rdclass} {rdtype} {rdata}" dnskeys.append(dnskey) - numkeys += _check_dnskeys(dnskeys, ksks, keydir=kskdir, cdnskey=cdnskey) + numkeys += _check_dnskeys(dnskeys, ksks, cdnskey=cdnskey) if not cdnskey: - numkeys += _check_dnskeys(dnskeys, zsks, keydir=zskdir) + numkeys += _check_dnskeys(dnskeys, zsks) assert numkeys == len(dnskeys) # pylint: disable=too-many-locals -def check_cds(rrset, keys, keydir=None): +def check_cds(rrset, keys): # Check if the correct CDS records are published. If the current time # is between the timing metadata 'publish' and 'delete', the key must have # a DNSKEY record published. If 'cdnskey' is True, check against CDNSKEY # records instead. - now = datetime.now().strftime("%Y%m%d%H%M%S") + now = KeyTimingMetadata.now() numcds = 0 cdss = [] @@ -430,21 +467,20 @@ def check_cds(rrset, keys, keydir=None): cdss.append(cds) for key in keys: - ksk, _ = get_keyrole(key, keydir=keydir) - assert ksk + assert key.is_ksk() - publish = get_timing_metadata(key, "SyncPublish", keydir=keydir) - delete = get_timing_metadata(key, "SyncDelete", keydir=keydir, must_exist=False) - published = int(now) >= int(publish) - removed = int(delete) != 0 and int(delete) <= int(now) + publish = key.get_timing("SyncPublish") + delete = key.get_timing("SyncDelete", must_exist=False) + published = now >= publish + removed = delete is not None and delete <= now if not published or removed: for cds in cdss: - assert not cds_equals(key, cds, "SHA-256", keydir=keydir) + assert not key.cds_equals(cds, "SHA-256") continue has_cds = False for cds in cdss: - if cds_equals(key, cds, "SHA-256", keydir=keydir): + if key.cds_equals(cds, "SHA-256"): has_cds = True break @@ -475,8 +511,7 @@ def _query_rrset(server, fqdn, qtype): return rrs, rrsigs -# pylint: disable=too-many-arguments -def check_apex(server, zone, ksks, zsks, kskdir=None, zskdir=None): +def check_apex(server, zone, ksks, zsks): # Test the apex of a zone. This checks that the SOA and DNSKEY RRsets # are signed correctly and with the appropriate keys. fqdn = f"{zone}." @@ -484,42 +519,33 @@ def check_apex(server, zone, ksks, zsks, kskdir=None, zskdir=None): # test dnskey query dnskeys, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.DNSKEY) assert len(dnskeys) > 0 - check_dnskeys(dnskeys, ksks, zsks, kskdir=kskdir, zskdir=zskdir) + check_dnskeys(dnskeys, ksks, zsks) assert len(rrsigs) > 0 - check_signatures( - rrsigs, dns.rdatatype.DNSKEY, fqdn, ksks, zsks, kskdir=kskdir, zskdir=zskdir - ) + check_signatures(rrsigs, dns.rdatatype.DNSKEY, fqdn, ksks, zsks) # test soa query soa, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.SOA) assert len(soa) == 1 assert f"{zone}. {DEFAULT_TTL} IN SOA" in soa[0].to_text() assert len(rrsigs) > 0 - check_signatures( - rrsigs, dns.rdatatype.SOA, fqdn, ksks, zsks, kskdir=kskdir, zskdir=zskdir - ) + check_signatures(rrsigs, dns.rdatatype.SOA, fqdn, ksks, zsks) # test cdnskey query cdnskeys, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.CDNSKEY) assert len(cdnskeys) > 0 - check_dnskeys(cdnskeys, ksks, zsks, kskdir=kskdir, zskdir=zskdir, cdnskey=True) + check_dnskeys(cdnskeys, ksks, zsks, cdnskey=True) assert len(rrsigs) > 0 - check_signatures( - rrsigs, dns.rdatatype.CDNSKEY, fqdn, ksks, zsks, kskdir=kskdir, zskdir=zskdir - ) + check_signatures(rrsigs, dns.rdatatype.CDNSKEY, fqdn, ksks, zsks) # test cds query cds, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.CDS) assert len(cds) > 0 - check_cds(cds, ksks, keydir=kskdir) + check_cds(cds, ksks) assert len(rrsigs) > 0 - check_signatures( - rrsigs, dns.rdatatype.CDS, fqdn, ksks, zsks, kskdir=kskdir, zskdir=zskdir - ) + check_signatures(rrsigs, dns.rdatatype.CDS, fqdn, ksks, zsks) -# pylint: disable=too-many-arguments -def check_subdomain(server, zone, ksks, zsks, kskdir=None, zskdir=None): +def check_subdomain(server, zone, ksks, zsks): # Test an RRset below the apex and verify it is signed correctly. fqdn = f"{zone}." qname = f"a.{zone}." @@ -538,4 +564,4 @@ def check_subdomain(server, zone, ksks, zsks, kskdir=None, zskdir=None): assert match in rrset.to_text() assert len(rrsigs) > 0 - check_signatures(rrsigs, qtype, fqdn, ksks, zsks, kskdir=kskdir, zskdir=zskdir) + check_signatures(rrsigs, qtype, fqdn, ksks, zsks) diff --git a/bin/tests/system/ksr/tests_ksr.py b/bin/tests/system/ksr/tests_ksr.py index efe688d983..73a0fa0fb3 100644 --- a/bin/tests/system/ksr/tests_ksr.py +++ b/bin/tests/system/ksr/tests_ksr.py @@ -11,29 +11,26 @@ # pylint: disable=too-many-lines +from datetime import timedelta import os import shutil import time +from typing import List, Optional from datetime import datetime import isctest - from isctest.kasp import ( - addtime, - cds_equals, - dnskey_equals, - get_keytag, - get_metadata, - get_timing_metadata, + Key, + KeyTimingMetadata, ) def between(value, start, end): - if int(value) == 0: + if value is None or start is None or end is None: return False - return int(value) > int(start) and int(value) < int(end) + return start < value < end def file_contents_equal(file1, file2): @@ -46,6 +43,10 @@ def file_contents_equal(file1, file2): isctest.run.cmd(diff_command) +def keystr_to_keylist(keystr: str, keydir: Optional[str] = None) -> List[Key]: + return [Key(name, keydir) for name in keystr.split()] + + def keygen(zone, policy, keydir, when="now"): keygen_command = [ *os.environ.get("KEYGEN").split(), @@ -65,9 +66,7 @@ def keygen(zone, policy, keydir, when="now"): when, zone, ] - output = isctest.run.cmd(keygen_command, log_stdout=True).stdout.decode("utf-8") - keys = output.split() - return keys + return isctest.run.cmd(keygen_command, log_stdout=True).stdout.decode("utf-8") def ksr(zone, policy, action, options="", raise_on_exception=True): @@ -89,21 +88,15 @@ def ksr(zone, policy, action, options="", raise_on_exception=True): # pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-statements -def check_keys(keys, lifetime, alg, size, keydir=None, offset=0, with_state=False): +def check_keys(keys, lifetime, alg, size, offset=0, with_state=False): # Check keys that were created. - inception = 0 num = 0 - now = datetime.now().strftime("%Y%m%d%H%M%S") + now = KeyTimingMetadata.now() for key in keys: - if keydir is not None: - statefile = f"{keydir}/{key}.state" - else: - statefile = f"{key}.state" - # created: from keyfile plus offset - created = get_timing_metadata(key, "Created", keydir=keydir, offset=offset) + created = key.get_timing("Created") + offset # active: retired previous key if num == 0: @@ -111,23 +104,22 @@ def check_keys(keys, lifetime, alg, size, keydir=None, offset=0, with_state=Fals else: active = retired - # published: 2h5m (dnskey-ttl + publish-safety + propagation) - published = addtime(active, -7500) + # published: dnskey-ttl + publish-safety + propagation + published = active - timedelta(hours=2, minutes=5) # retired: zsk-lifetime - if lifetime > 0: - retired = addtime(active, lifetime) - # removed: 10d1h5m - # (ttlsig + retire-safety + sign-delay + propagation) - removed = addtime(retired, 867900) + if lifetime is not None: + retired = active + lifetime + # removed: ttlsig + retire-safety + sign-delay + propagation + removed = retired + timedelta(days=10, hours=1, minutes=5) else: - retired = 0 - removed = 0 + retired = None + removed = None - if between(now, published, retired) or int(retired) == 0: + if retired is None or between(now, published, retired): goal = "omnipresent" - pubdelay = addtime(published, 7500) - signdelay = addtime(active, 867900) + pubdelay = published + timedelta(hours=2, minutes=5) + signdelay = active + timedelta(days=10, hours=1, minutes=5) if between(now, published, pubdelay): state_dnskey = "rumoured" @@ -143,20 +135,21 @@ def check_keys(keys, lifetime, alg, size, keydir=None, offset=0, with_state=Fals state_dnskey = "hidden" state_zrrsig = "hidden" - with open(statefile, "r", encoding="utf-8") as file: + with open(key.statefile, "r", encoding="utf-8") as file: metadata = file.read() assert f"Algorithm: {alg}" in metadata assert f"Length: {size}" in metadata - assert f"Lifetime: {lifetime}" in metadata assert "KSK: no" in metadata assert "ZSK: yes" in metadata assert f"Published: {published}" in metadata assert f"Active: {active}" in metadata - if lifetime > 0: + if lifetime is not None: assert f"Retired: {retired}" in metadata assert f"Removed: {removed}" in metadata + assert f"Lifetime: {int(lifetime.total_seconds())}" in metadata else: + assert "Lifetime: 0" in metadata assert "Retired:" not in metadata assert "Removed:" not in metadata @@ -167,39 +160,36 @@ def check_keys(keys, lifetime, alg, size, keydir=None, offset=0, with_state=Fals assert "KRRSIGState:" not in metadata assert "DSState:" not in metadata - inception += lifetime num += 1 -def check_keysigningrequest(out, zsks, start, end, keydir=None): +def check_keysigningrequest(out, zsks, start, end): lines = out.split("\n") line_no = 0 inception = start - while int(inception) < int(end): - next_bundle = addtime(end, 1) + while inception < end: + next_bundle = end + 1 # expect bundle header assert f";; KeySigningRequest 1.0 {inception}" in lines[line_no] line_no += 1 # expect zsks for key in sorted(zsks): - published = get_timing_metadata(key, "Publish", keydir=keydir) + published = key.get_timing("Publish") if between(published, inception, next_bundle): next_bundle = published - removed = get_timing_metadata( - key, "Delete", keydir=keydir, must_exist=False - ) + removed = key.get_timing("Delete", must_exist=False) if between(removed, inception, next_bundle): next_bundle = removed - if int(published) > int(inception): + if published > inception: continue - if int(removed) != 0 and int(inception) >= int(removed): + if removed is not None and inception >= removed: continue # this zsk must be in the ksr - assert dnskey_equals(key, lines[line_no], keydir=keydir) + assert key.dnskey_equals(lines[line_no]) line_no += 1 inception = next_bundle @@ -225,17 +215,15 @@ def check_signedkeyresponse( start, end, refresh, - kskdir=None, - zskdir=None, cdnskey=True, cds="SHA-256", ): lines = out.split("\n") line_no = 0 - next_bundle = addtime(end, 1) + next_bundle = end + 1 inception = start - while int(inception) < int(end): + while inception < end: # A single signed key response may consist of: # ;; SignedKeyResponse (header) # ;; DNSKEY 257 (one per published key in ksks) @@ -246,9 +234,9 @@ def check_signedkeyresponse( # ;; CDS (one per published key in ksks) # ;; RRSIG(CDS) (one per active key in ksks) - sigstart = addtime(inception, -3600) # clockskew: 1 hour - sigend = addtime(inception, 1209600) # sig-validity: 14 days - next_bundle = addtime(sigend, refresh) + sigstart = inception - timedelta(hours=1) # clockskew + sigend = inception + timedelta(days=14) # sig-validity + next_bundle = sigend + refresh # ignore empty lines while line_no < len(lines): @@ -263,56 +251,49 @@ def check_signedkeyresponse( # expect ksks for key in sorted(ksks): - published = get_timing_metadata(key, "Publish", keydir=kskdir) - removed = get_timing_metadata( - key, "Delete", keydir=kskdir, must_exist=False - ) + published = key.get_timing("Publish") + removed = key.get_timing("Delete", must_exist=False) - if int(published) > int(inception): + if published > inception: continue - if int(removed) != 0 and int(inception) >= int(removed): + if removed is not None and inception >= removed: continue # this ksk must be in the ksr - assert dnskey_equals(key, lines[line_no], keydir=kskdir) + assert key.dnskey_equals(lines[line_no]) line_no += 1 # expect zsks for key in sorted(zsks): - published = get_timing_metadata(key, "Publish", keydir=zskdir) + published = key.get_timing("Publish") if between(published, inception, next_bundle): next_bundle = published - removed = get_timing_metadata( - key, "Delete", keydir=zskdir, must_exist=False - ) + removed = key.get_timing("Delete", must_exist=False) if between(removed, inception, next_bundle): next_bundle = removed - if int(published) > int(inception): + if published > inception: continue - if int(removed) != 0 and int(inception) >= int(removed): + if removed is not None and inception >= removed: continue # this zsk must be in the ksr - assert dnskey_equals(key, lines[line_no], keydir=zskdir) + assert key.dnskey_equals(lines[line_no]) line_no += 1 # expect rrsig(dnskey) for key in sorted(ksks): - active = get_timing_metadata(key, "Activate", keydir=kskdir) - inactive = get_timing_metadata( - key, "Inactive", keydir=kskdir, must_exist=False - ) - if int(active) > int(inception): + active = key.get_timing("Activate") + inactive = key.get_timing("Inactive", must_exist=False) + if active > inception: continue - if int(inactive) != 0 and int(inception) >= int(inactive): + if inactive is not None and inception >= inactive: continue # there must be a signature of this ksk - keytag = get_keytag(key) - alg = get_metadata(key, "Algorithm", keydir=kskdir) - expect = f"{zone}. 3600 IN RRSIG DNSKEY {alg} 2 3600 {sigend} {sigstart} {keytag} {zone}." + alg = key.get_metadata("Algorithm") + expect = f"{zone}. 3600 IN RRSIG DNSKEY {alg} 2 3600 {sigend} {sigstart} {key.tag} {zone}." rrsig = " ".join(lines[line_no].split()) assert expect in rrsig line_no += 1 @@ -320,34 +301,29 @@ def check_signedkeyresponse( # expect cdnskey if cdnskey: for key in sorted(ksks): - published = get_timing_metadata(key, "Publish", keydir=kskdir) - removed = get_timing_metadata( - key, "Delete", keydir=kskdir, must_exist=False - ) - if int(published) > int(inception): + published = key.get_timing("Publish") + removed = key.get_timing("Delete", must_exist=False) + if published > inception: continue - if int(removed) != 0 and int(inception) >= int(removed): + if removed is not None and inception >= removed: continue # the cdnskey of this ksk must be in the ksr - assert dnskey_equals(key, lines[line_no], keydir=kskdir, cdnskey=True) + assert key.dnskey_equals(lines[line_no], cdnskey=True) line_no += 1 # expect rrsig(cdnskey) for key in sorted(ksks): - active = get_timing_metadata(key, "Activate", keydir=kskdir) - inactive = get_timing_metadata( - key, "Inactive", keydir=kskdir, must_exist=False - ) - if int(active) > int(inception): + active = key.get_timing("Activate") + inactive = key.get_timing("Inactive", must_exist=False) + if active > inception: continue - if int(inactive) != 0 and int(inception) >= int(inactive): + if inactive is not None and inception >= inactive: continue # there must be a signature of this ksk - keytag = get_keytag(key) - alg = get_metadata(key, "Algorithm", keydir=kskdir) - expect = f"{zone}. 3600 IN RRSIG CDNSKEY {alg} 2 3600 {sigend} {sigstart} {keytag} {zone}." + alg = key.get_metadata("Algorithm") + expect = f"{zone}. 3600 IN RRSIG CDNSKEY {alg} 2 3600 {sigend} {sigstart} {key.tag} {zone}." rrsig = " ".join(lines[line_no].split()) assert expect in rrsig line_no += 1 @@ -355,36 +331,31 @@ def check_signedkeyresponse( # expect cds if cds != "": for key in sorted(ksks): - published = get_timing_metadata(key, "Publish", keydir=kskdir) - removed = get_timing_metadata( - key, "Delete", keydir=kskdir, must_exist=False - ) - if int(published) > int(inception): + published = key.get_timing("Publish") + removed = key.get_timing("Delete", must_exist=False) + if published > inception: continue - if int(removed) != 0 and int(inception) >= int(removed): + if removed is not None and inception >= removed: continue # the cds of this ksk must be in the ksr expected_cds = cds.split(",") for alg in expected_cds: - assert cds_equals(key, lines[line_no], alg.strip(), keydir=kskdir) + assert key.cds_equals(lines[line_no], alg.strip()) line_no += 1 # expect rrsig(cds) for key in sorted(ksks): - active = get_timing_metadata(key, "Activate", keydir=kskdir) - inactive = get_timing_metadata( - key, "Inactive", keydir=kskdir, must_exist=False - ) - if int(active) > int(inception): + active = key.get_timing("Activate") + inactive = key.get_timing("Inactive", must_exist=False) + if active > inception: continue - if int(inactive) != 0 and int(inception) >= int(inactive): + if inactive is not None and inception >= inactive: continue # there must be a signature of this ksk - keytag = get_keytag(key) - alg = get_metadata(key, "Algorithm", keydir=kskdir) - expect = f"{zone}. 3600 IN RRSIG CDS {alg} 2 3600 {sigend} {sigstart} {keytag} {zone}." + alg = key.get_metadata("Algorithm") + expect = f"{zone}. 3600 IN RRSIG CDS {alg} 2 3600 {sigend} {sigstart} {key.tag} {zone}." rrsig = " ".join(lines[line_no].split()) assert expect in rrsig line_no += 1 @@ -441,52 +412,55 @@ def test_ksr_common(servers): n = 1 # create ksk - ksks = keygen(zone, policy, "ns1/offline") + kskdir = "ns1/offline" + out = keygen(zone, policy, kskdir) + ksks = keystr_to_keylist(out, kskdir) assert len(ksks) == 1 # check that 'dnssec-ksr keygen' pregenerates right amount of keys out, _ = ksr(zone, policy, "keygen", options="-i now -e +1y") - zsks = out.split() + zsks = keystr_to_keylist(out) assert len(zsks) == 2 alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") size = os.environ.get("DEFAULT_BITS") - lifetime = 16070400 + lifetime = timedelta(days=31 * 6) check_keys(zsks, lifetime, alg, size) # check that 'dnssec-ksr keygen' pregenerates right amount of keys # in the given key directory - out, _ = ksr(zone, policy, "keygen", options="-K ns1 -i now -e +1y") - zsks = out.split() + zskdir = "ns1" + out, _ = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i now -e +1y") + zsks = keystr_to_keylist(out, zskdir) assert len(zsks) == 2 alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") size = os.environ.get("DEFAULT_BITS") - lifetime = 16070400 - check_keys(zsks, lifetime, alg, size, keydir="ns1") + lifetime = timedelta(days=31 * 6) + check_keys(zsks, lifetime, alg, size) for key in zsks: - privatefile = f"ns1/{key}.private" - keyfile = f"ns1/{key}.key" - statefile = f"ns1/{key}.state" + privatefile = f"{key.path}.private" + keyfile = f"{key.path}.key" + statefile = f"{key.path}.state" shutil.copyfile(privatefile, f"{privatefile}.backup") shutil.copyfile(keyfile, f"{keyfile}.backup") shutil.copyfile(statefile, f"{statefile}.backup") # check that 'dnssec-ksr request' creates correct ksr - now = get_timing_metadata(zsks[0], "Created", keydir="ns1") - until = addtime(now, 31536000) # 1 year - out, _ = ksr(zone, policy, "request", options=f"-K ns1 -i {now} -e +1y") + now = zsks[0].get_timing("Created") + until = now + timedelta(days=365) + out, _ = ksr(zone, policy, "request", options=f"-K {zskdir} -i {now} -e +1y") fname = f"{zone}.ksr.{n}" with open(fname, "w", encoding="utf-8") as file: file.write(out) - check_keysigningrequest(out, zsks, now, until, keydir="ns1") + check_keysigningrequest(out, zsks, now, until) # check that 'dnssec-ksr sign' creates correct skr out, _ = ksr( - zone, policy, "sign", options=f"-K ns1/offline -f {fname} -i {now} -e +1y" + zone, policy, "sign", options=f"-K {kskdir} -f {fname} -i {now} -e +1y" ) fname = f"{zone}.skr.{n}" @@ -494,28 +468,26 @@ def test_ksr_common(servers): file.write(out) refresh = -432000 # 5 days - check_signedkeyresponse( - out, zone, ksks, zsks, now, until, refresh, kskdir="ns1/offline", zskdir="ns1" - ) + check_signedkeyresponse(out, zone, ksks, zsks, now, until, refresh) # common test cases (2) n = 2 # check that 'dnssec-ksr keygen' selects pregenerated keys for # the same time bundle - out, _ = ksr(zone, policy, "keygen", options=f"-K ns1 -i {now} -e +1y") - selected_zsks = out.split() + out, _ = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i {now} -e +1y") + selected_zsks = keystr_to_keylist(out, zskdir) assert len(selected_zsks) == 2 for index, key in enumerate(selected_zsks): assert zsks[index] == key - file_contents_equal(f"ns1/{key}.private", f"ns1/{key}.private.backup") - file_contents_equal(f"ns1/{key}.key", f"ns1/{key}.key.backup") - file_contents_equal(f"ns1/{key}.state", f"ns1/{key}.state.backup") + file_contents_equal(f"{key.path}.private", f"{key.path}.private.backup") + file_contents_equal(f"{key.path}.key", f"{key.path}.key.backup") + file_contents_equal(f"{key.path}.state", f"{key.path}.state.backup") # check that 'dnssec-ksr keygen' generates only necessary keys for # overlapping time bundle - out, err = ksr(zone, policy, "keygen", options=f"-K ns1 -i {now} -e +2y -v 1") - overlapping_zsks = out.split() + out, err = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i {now} -e +2y -v 1") + overlapping_zsks = keystr_to_keylist(out, zskdir) assert len(overlapping_zsks) == 4 verbose = err.split() @@ -531,15 +503,15 @@ def test_ksr_common(servers): for index, key in enumerate(overlapping_zsks): if index < 2: assert zsks[index] == key - file_contents_equal(f"ns1/{key}.private", f"ns1/{key}.private.backup") - file_contents_equal(f"ns1/{key}.key", f"ns1/{key}.key.backup") - file_contents_equal(f"ns1/{key}.state", f"ns1/{key}.state.backup") + file_contents_equal(f"{key.path}.private", f"{key.path}.private.backup") + file_contents_equal(f"{key.path}.key", f"{key.path}.key.backup") + file_contents_equal(f"{key.path}.state", f"{key.path}.state.backup") # run 'dnssec-ksr keygen' again with verbosity 0 - out, _ = ksr(zone, policy, "keygen", options=f"-K ns1 -i {now} -e +2y") - overlapping_zsks2 = out.split() + out, _ = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i {now} -e +2y") + overlapping_zsks2 = keystr_to_keylist(out, zskdir) assert len(overlapping_zsks2) == 4 - check_keys(overlapping_zsks2, lifetime, alg, size, keydir="ns1") + check_keys(overlapping_zsks2, lifetime, alg, size) for index, key in enumerate(overlapping_zsks2): assert overlapping_zsks[index] == key @@ -551,7 +523,7 @@ def test_ksr_common(servers): with open(fname, "w", encoding="utf-8") as file: file.write(out) - check_keysigningrequest(out, zsks, now, until, keydir="ns1") + check_keysigningrequest(out, zsks, now, until) # check that 'dnssec-ksr request' creates correct ksr with new interval out, _ = ksr(zone, policy, "request", options=f"-K ns1 -i {now} -e +2y") @@ -560,8 +532,8 @@ def test_ksr_common(servers): with open(fname, "w", encoding="utf-8") as file: file.write(out) - until = addtime(now, 63072000) # 2 years - check_keysigningrequest(out, overlapping_zsks, now, until, keydir="ns1") + until = now + timedelta(days=365 * 2) + check_keysigningrequest(out, overlapping_zsks, now, until) # check that 'dnssec-ksr request' errors if there are not enough keys _, err = ksr( @@ -592,8 +564,6 @@ def test_ksr_common(servers): now, until, refresh, - kskdir="ns1/offline", - zskdir="ns1", ) # add zone @@ -618,15 +588,11 @@ def test_ksr_common(servers): # - dnssec_verify isctest.kasp.dnssec_verify(ns1, zone) # - check keys - check_keys(overlapping_zsks, lifetime, alg, size, keydir="ns1", with_state=True) + check_keys(overlapping_zsks, lifetime, alg, size, with_state=True) # - check apex - isctest.kasp.check_apex( - ns1, zone, ksks, overlapping_zsks, kskdir="ns1/offline", zskdir="ns1" - ) + isctest.kasp.check_apex(ns1, zone, ksks, overlapping_zsks) # - check subdomain - isctest.kasp.check_subdomain( - ns1, zone, ksks, overlapping_zsks, kskdir="ns1/offline", zskdir="ns1" - ) + isctest.kasp.check_subdomain(ns1, zone, ksks, overlapping_zsks) # pylint: disable=too-many-locals @@ -636,38 +602,39 @@ def test_ksr_lastbundle(servers): n = 1 # create ksk - now = datetime.now().strftime("%Y%m%d%H%M%S") - offset = -31536000 - when = addtime(now, offset) - when = addtime(when, -86400) - ksks = keygen(zone, policy, "ns1/offline", when=when) + kskdir = "ns1/offline" + now = KeyTimingMetadata.now() + offset = -timedelta(days=365) + when = now + offset - timedelta(days=1) + out = keygen(zone, policy, kskdir, when=str(when)) + ksks = keystr_to_keylist(out, kskdir) assert len(ksks) == 1 # check that 'dnssec-ksr keygen' pregenerates right amount of keys - out, _ = ksr(zone, policy, "keygen", options="-K ns1 -i -1y -e +1d") - zsks = out.split() + zskdir = "ns1" + out, _ = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i -1y -e +1d") + zsks = keystr_to_keylist(out, zskdir) assert len(zsks) == 2 alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") size = os.environ.get("DEFAULT_BITS") - lifetime = 16070400 - check_keys(zsks, lifetime, alg, size, keydir="ns1", offset=offset) + lifetime = timedelta(days=31 * 6) + check_keys(zsks, lifetime, alg, size, offset=offset) # check that 'dnssec-ksr request' creates correct ksr - then = get_timing_metadata(zsks[0], "Created", keydir="ns1") - then = addtime(then, offset) - until = addtime(then, 31622400) # 1 year, 1 day - out, _ = ksr(zone, policy, "request", options=f"-K ns1 -i {then} -e +1d") + then = zsks[0].get_timing("Created") + offset + until = then + timedelta(days=366) + out, _ = ksr(zone, policy, "request", options=f"-K {zskdir} -i {then} -e +1d") fname = f"{zone}.ksr.{n}" with open(fname, "w", encoding="utf-8") as file: file.write(out) - check_keysigningrequest(out, zsks, then, until, keydir="ns1") + check_keysigningrequest(out, zsks, then, until) # check that 'dnssec-ksr sign' creates correct skr out, _ = ksr( - zone, policy, "sign", options=f"-K ns1/offline -f {fname} -i {then} -e +1d" + zone, policy, "sign", options=f"-K {kskdir} -f {fname} -i {then} -e +1d" ) fname = f"{zone}.skr.{n}" @@ -675,9 +642,7 @@ def test_ksr_lastbundle(servers): file.write(out) refresh = -432000 # 5 days - check_signedkeyresponse( - out, zone, ksks, zsks, then, until, refresh, kskdir="ns1/offline", zskdir="ns1" - ) + check_signedkeyresponse(out, zone, ksks, zsks, then, until, refresh) # add zone ns1 = servers["ns1"] @@ -701,13 +666,11 @@ def test_ksr_lastbundle(servers): # - dnssec_verify isctest.kasp.dnssec_verify(ns1, zone) # - check keys - check_keys(zsks, lifetime, alg, size, keydir="ns1", offset=offset, with_state=True) + check_keys(zsks, lifetime, alg, size, offset=offset, with_state=True) # - check apex - isctest.kasp.check_apex(ns1, zone, ksks, zsks, kskdir="ns1/offline", zskdir="ns1") + isctest.kasp.check_apex(ns1, zone, ksks, zsks) # - check subdomain - isctest.kasp.check_subdomain( - ns1, zone, ksks, zsks, kskdir="ns1/offline", zskdir="ns1" - ) + isctest.kasp.check_subdomain(ns1, zone, ksks, zsks) # check that last bundle warning is logged warning = "last bundle in skr, please import new skr file" @@ -721,38 +684,40 @@ def test_ksr_inthemiddle(servers): n = 1 # create ksk - now = datetime.now().strftime("%Y%m%d%H%M%S") - offset = -31536000 - when = addtime(now, offset) - when = addtime(when, -86400) - ksks = keygen(zone, policy, "ns1/offline", when=when) + kskdir = "ns1/offline" + now = KeyTimingMetadata.now() + offset = -timedelta(days=365) + when = now + offset - timedelta(days=1) + out = keygen(zone, policy, kskdir, when=str(when)) + ksks = keystr_to_keylist(out, kskdir) assert len(ksks) == 1 # check that 'dnssec-ksr keygen' pregenerates right amount of keys - out, _ = ksr(zone, policy, "keygen", options="-K ns1 -i -1y -e +1y") - zsks = out.split() + zskdir = "ns1" + out, _ = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i -1y -e +1y") + zsks = keystr_to_keylist(out, zskdir) assert len(zsks) == 4 alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") size = os.environ.get("DEFAULT_BITS") - lifetime = 16070400 - check_keys(zsks, lifetime, alg, size, keydir="ns1", offset=offset) + lifetime = timedelta(days=31 * 6) + check_keys(zsks, lifetime, alg, size, offset=offset) # check that 'dnssec-ksr request' creates correct ksr - then = get_timing_metadata(zsks[0], "Created", keydir="ns1") - then = addtime(then, offset) - until = addtime(then, 63072000) # 2 years - out, _ = ksr(zone, policy, "request", options=f"-K ns1 -i {then} -e +1y") + then = zsks[0].get_timing("Created") + then = then + offset + until = then + timedelta(days=365 * 2) + out, _ = ksr(zone, policy, "request", options=f"-K {zskdir} -i {then} -e +1y") fname = f"{zone}.ksr.{n}" with open(fname, "w", encoding="utf-8") as file: file.write(out) - check_keysigningrequest(out, zsks, then, until, keydir="ns1") + check_keysigningrequest(out, zsks, then, until) # check that 'dnssec-ksr sign' creates correct skr out, _ = ksr( - zone, policy, "sign", options=f"-K ns1/offline -f {fname} -i {then} -e +1y" + zone, policy, "sign", options=f"-K {kskdir} -f {fname} -i {then} -e +1y" ) fname = f"{zone}.skr.{n}" @@ -760,9 +725,7 @@ def test_ksr_inthemiddle(servers): file.write(out) refresh = -432000 # 5 days - check_signedkeyresponse( - out, zone, ksks, zsks, then, until, refresh, kskdir="ns1/offline", zskdir="ns1" - ) + check_signedkeyresponse(out, zone, ksks, zsks, then, until, refresh) # add zone ns1 = servers["ns1"] @@ -786,13 +749,11 @@ def test_ksr_inthemiddle(servers): # - dnssec_verify isctest.kasp.dnssec_verify(ns1, zone) # - check keys - check_keys(zsks, lifetime, alg, size, keydir="ns1", offset=offset, with_state=True) + check_keys(zsks, lifetime, alg, size, offset=offset, with_state=True) # - check apex - isctest.kasp.check_apex(ns1, zone, ksks, zsks, kskdir="ns1/offline", zskdir="ns1") + isctest.kasp.check_apex(ns1, zone, ksks, zsks) # - check subdomain - isctest.kasp.check_subdomain( - ns1, zone, ksks, zsks, kskdir="ns1/offline", zskdir="ns1" - ) + isctest.kasp.check_subdomain(ns1, zone, ksks, zsks) # check that no last bundle warning is logged warning = "last bundle in skr, please import new skr file" @@ -804,22 +765,25 @@ def check_ksr_rekey_logs_error(server, zone, policy, offset, end): n = 1 # create ksk - now = datetime.now().strftime("%Y%m%d%H%M%S") - then = addtime(now, offset) - until = addtime(now, end) - ksks = keygen(zone, policy, "ns1/offline", when=then) + kskdir = "ns1/offline" + now = KeyTimingMetadata.now() + then = now + offset + until = now + end + out = keygen(zone, policy, kskdir, when=str(then)) + ksks = keystr_to_keylist(out, kskdir) assert len(ksks) == 1 # key generation - out, _ = ksr(zone, policy, "keygen", options=f"-K ns1 -i {then} -e {until}") - zsks = out.split() + zskdir = "ns1" + out, _ = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i {then} -e {until}") + zsks = keystr_to_keylist(out, zskdir) assert len(zsks) == 2 # create request - now = get_timing_metadata(zsks[0], "Created", keydir="ns1") - then = addtime(now, offset) - until = addtime(now, end) - out, _ = ksr(zone, policy, "request", options=f"-K ns1 -i {then} -e {until}") + now = zsks[0].get_timing("Created") + then = now + offset + until = now + end + out, _ = ksr(zone, policy, "request", options=f"-K {zskdir} -i {then} -e {until}") fname = f"{zone}.ksr.{n}" with open(fname, "w", encoding="utf-8") as file: @@ -827,7 +791,7 @@ def check_ksr_rekey_logs_error(server, zone, policy, offset, end): # sign request out, _ = ksr( - zone, policy, "sign", options=f"-K ns1/offline -f {fname} -i {then} -e {until}" + zone, policy, "sign", options=f"-K {kskdir} -f {fname} -i {then} -e {until}" ) fname = f"{zone}.skr.{n}" @@ -878,33 +842,36 @@ def test_ksr_unlimited(servers): n = 1 # create ksk - ksks = keygen(zone, policy, "ns1/offline") + kskdir = "ns1/offline" + out = keygen(zone, policy, kskdir) + ksks = keystr_to_keylist(out, kskdir) assert len(ksks) == 1 # check that 'dnssec-ksr keygen' pregenerates right amount of keys - out, _ = ksr(zone, policy, "keygen", options="-K ns1 -i now -e +2y") - zsks = out.split() + zskdir = "ns1" + out, _ = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i now -e +2y") + zsks = keystr_to_keylist(out, zskdir) assert len(zsks) == 1 alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") size = os.environ.get("DEFAULT_BITS") - lifetime = 0 - check_keys(zsks, lifetime, alg, size, keydir="ns1") + lifetime = None + check_keys(zsks, lifetime, alg, size) # check that 'dnssec-ksr request' creates correct ksr - now = get_timing_metadata(zsks[0], "Created", keydir="ns1") - until = addtime(now, 4 * 31536000) # 4 years - out, _ = ksr(zone, policy, "request", options=f"-K ns1 -i {now} -e +4y") + now = zsks[0].get_timing("Created") + until = now + timedelta(days=365 * 4) + out, _ = ksr(zone, policy, "request", options=f"-K {zskdir} -i {now} -e +4y") fname = f"{zone}.ksr.{n}" with open(fname, "w", encoding="utf-8") as file: file.write(out) - check_keysigningrequest(out, zsks, now, until, keydir="ns1") + check_keysigningrequest(out, zsks, now, until) # check that 'dnssec-ksr sign' creates correct skr without cdnskey out, _ = ksr( - zone, "no-cdnskey", "sign", options=f"-K ns1/offline -f {fname} -i {now} -e +4y" + zone, "no-cdnskey", "sign", options=f"-K {kskdir} -f {fname} -i {now} -e +4y" ) skrfile = f"{zone}.no-cdnskey.skr.{n}" @@ -920,15 +887,13 @@ def test_ksr_unlimited(servers): now, until, refresh, - kskdir="ns1/offline", - zskdir="ns1", cdnskey=False, cds="SHA-1, SHA-256, SHA-384", ) # check that 'dnssec-ksr sign' creates correct skr without cds out, _ = ksr( - zone, "no-cds", "sign", options=f"-K ns1/offline -f {fname} -i {now} -e +4y" + zone, "no-cds", "sign", options=f"-K {kskdir} -f {fname} -i {now} -e +4y" ) skrfile = f"{zone}.no-cds.skr.{n}" @@ -944,14 +909,12 @@ def test_ksr_unlimited(servers): now, until, refresh, - kskdir="ns1/offline", - zskdir="ns1", cds="", ) # check that 'dnssec-ksr sign' creates correct skr out, _ = ksr( - zone, policy, "sign", options=f"-K ns1/offline -f {fname} -i {now} -e +4y" + zone, policy, "sign", options=f"-K {kskdir} -f {fname} -i {now} -e +4y" ) skrfile = f"{zone}.{policy}.skr.{n}" @@ -959,9 +922,7 @@ def test_ksr_unlimited(servers): file.write(out) refresh = -432000 # 5 days - check_signedkeyresponse( - out, zone, ksks, zsks, now, until, refresh, kskdir="ns1/offline", zskdir="ns1" - ) + check_signedkeyresponse(out, zone, ksks, zsks, now, until, refresh) # add zone ns1 = servers["ns1"] @@ -985,13 +946,11 @@ def test_ksr_unlimited(servers): # - dnssec_verify isctest.kasp.dnssec_verify(ns1, zone) # - check keys - check_keys(zsks, lifetime, alg, size, keydir="ns1", with_state=True) + check_keys(zsks, lifetime, alg, size, with_state=True) # - check apex - isctest.kasp.check_apex(ns1, zone, ksks, zsks, kskdir="ns1/offline", zskdir="ns1") + isctest.kasp.check_apex(ns1, zone, ksks, zsks) # - check subdomain - isctest.kasp.check_subdomain( - ns1, zone, ksks, zsks, kskdir="ns1/offline", zskdir="ns1" - ) + isctest.kasp.check_subdomain(ns1, zone, ksks, zsks) # pylint: disable=too-many-locals @@ -1001,12 +960,15 @@ def test_ksr_twotone(servers): n = 1 # create ksk - ksks = keygen(zone, policy, "ns1/offline") + kskdir = "ns1/offline" + out = keygen(zone, policy, kskdir) + ksks = keystr_to_keylist(out, kskdir) assert len(ksks) == 2 # check that 'dnssec-ksr keygen' pregenerates right amount of keys - out, _ = ksr(zone, policy, "keygen", options="-K ns1 -i now -e +1y") - zsks = out.split() + zskdir = "ns1" + out, _ = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i now -e +1y") + zsks = keystr_to_keylist(out, zskdir) # First algorithm keys have a lifetime of 3 months, so there should # be 4 created keys. Second algorithm keys have a lifetime of 5 # months, so there should be 3 created keys. While only two time @@ -1017,7 +979,7 @@ def test_ksr_twotone(servers): zsks_defalg = [] zsks_altalg = [] for zsk in zsks: - alg = get_metadata(zsk, "Algorithm", keydir="ns1") + alg = zsk.get_metadata("Algorithm") if alg == os.environ.get("DEFAULT_ALGORITHM_NUMBER"): zsks_defalg.append(zsk) elif alg == os.environ.get("ALTERNATIVE_ALGORITHM_NUMBER"): @@ -1028,38 +990,36 @@ def test_ksr_twotone(servers): alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") size = os.environ.get("DEFAULT_BITS") - lifetime = 8035200 # 3 months - check_keys(zsks_defalg, lifetime, alg, size, keydir="ns1") + lifetime = timedelta(days=31 * 3) + check_keys(zsks_defalg, lifetime, alg, size) alg = os.environ.get("ALTERNATIVE_ALGORITHM_NUMBER") size = os.environ.get("ALTERNATIVE_BITS") - lifetime = 13392000 # 5 months - check_keys(zsks_altalg, lifetime, alg, size, keydir="ns1") + lifetime = timedelta(days=31 * 5) + check_keys(zsks_altalg, lifetime, alg, size) # check that 'dnssec-ksr request' creates correct ksr - now = get_timing_metadata(zsks[0], "Created", keydir="ns1") - until = addtime(now, 31536000) # 1 year - out, _ = ksr(zone, policy, "request", options=f"-K ns1 -i {now} -e +1y") + now = zsks[0].get_timing("Created") + until = now + timedelta(days=365) + out, _ = ksr(zone, policy, "request", options=f"-K {zskdir} -i {now} -e +1y") fname = f"{zone}.ksr.{n}" with open(fname, "w", encoding="utf-8") as file: file.write(out) - check_keysigningrequest(out, zsks, now, until, keydir="ns1") + check_keysigningrequest(out, zsks, now, until) # check that 'dnssec-ksr sign' creates correct skr out, _ = ksr( - zone, policy, "sign", options=f"-K ns1/offline -f {fname} -i {now} -e +1y" + zone, policy, "sign", options=f"-K {kskdir} -f {fname} -i {now} -e +1y" ) skrfile = f"{zone}.skr.{n}" with open(skrfile, "w", encoding="utf-8") as file: file.write(out) - refresh = -432000 # 5 days - check_signedkeyresponse( - out, zone, ksks, zsks, now, until, refresh, kskdir="ns1/offline", zskdir="ns1" - ) + refresh = -timedelta(days=5) + check_signedkeyresponse(out, zone, ksks, zsks, now, until, refresh) # add zone ns1 = servers["ns1"] @@ -1085,16 +1045,14 @@ def test_ksr_twotone(servers): # - check keys alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") size = os.environ.get("DEFAULT_BITS") - lifetime = 8035200 # 3 months - check_keys(zsks_defalg, lifetime, alg, size, keydir="ns1", with_state=True) + lifetime = timedelta(days=31 * 3) + check_keys(zsks_defalg, lifetime, alg, size, with_state=True) alg = os.environ.get("ALTERNATIVE_ALGORITHM_NUMBER") size = os.environ.get("ALTERNATIVE_BITS") - lifetime = 13392000 # 5 months - check_keys(zsks_altalg, lifetime, alg, size, keydir="ns1", with_state=True) + lifetime = timedelta(days=31 * 5) + check_keys(zsks_altalg, lifetime, alg, size, with_state=True) # - check apex - isctest.kasp.check_apex(ns1, zone, ksks, zsks, kskdir="ns1/offline", zskdir="ns1") + isctest.kasp.check_apex(ns1, zone, ksks, zsks) # - check subdomain - isctest.kasp.check_subdomain( - ns1, zone, ksks, zsks, kskdir="ns1/offline", zskdir="ns1" - ) + isctest.kasp.check_subdomain(ns1, zone, ksks, zsks) From e806d8c6f507e22d27f46a382a2154e8053b0937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Tue, 8 Oct 2024 14:09:05 +0200 Subject: [PATCH 05/12] Rename kasp function to check_* If a function is expected to assert / raise on failure (rather than return boolean), its name should start with "check_". (cherry picked from commit 67957d1f54e01bc3066effc0fa5b6486efb66178) --- bin/tests/system/isctest/kasp.py | 4 ++-- bin/tests/system/ksr/tests_ksr.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py index 6fbc57b3b5..a2bc24d926 100644 --- a/bin/tests/system/isctest/kasp.py +++ b/bin/tests/system/isctest/kasp.py @@ -232,7 +232,7 @@ class Key: return self.path -def zone_is_signed(server, zone): +def check_zone_is_signed(server, zone): addr = server.ip fqdn = f"{zone}." @@ -283,7 +283,7 @@ def zone_is_signed(server, zone): assert signed -def dnssec_verify(server, zone): +def check_dnssec_verify(server, zone): # Check if zone if DNSSEC valid with dnssec-verify. fqdn = f"{zone}." transfer = _query(server, fqdn, dns.rdatatype.AXFR) diff --git a/bin/tests/system/ksr/tests_ksr.py b/bin/tests/system/ksr/tests_ksr.py index 73a0fa0fb3..fa61c3ef87 100644 --- a/bin/tests/system/ksr/tests_ksr.py +++ b/bin/tests/system/ksr/tests_ksr.py @@ -584,9 +584,9 @@ def test_ksr_common(servers): # - check rndc dnssec -status output isctest.kasp.check_dnssecstatus(ns1, zone, overlapping_zsks, policy=policy) # - zone is signed - isctest.kasp.zone_is_signed(ns1, zone) + isctest.kasp.check_zone_is_signed(ns1, zone) # - dnssec_verify - isctest.kasp.dnssec_verify(ns1, zone) + isctest.kasp.check_dnssec_verify(ns1, zone) # - check keys check_keys(overlapping_zsks, lifetime, alg, size, with_state=True) # - check apex @@ -662,9 +662,9 @@ def test_ksr_lastbundle(servers): # - check rndc dnssec -status output isctest.kasp.check_dnssecstatus(ns1, zone, zsks, policy=policy) # - zone is signed - isctest.kasp.zone_is_signed(ns1, zone) + isctest.kasp.check_zone_is_signed(ns1, zone) # - dnssec_verify - isctest.kasp.dnssec_verify(ns1, zone) + isctest.kasp.check_dnssec_verify(ns1, zone) # - check keys check_keys(zsks, lifetime, alg, size, offset=offset, with_state=True) # - check apex @@ -745,9 +745,9 @@ def test_ksr_inthemiddle(servers): # - check rndc dnssec -status output isctest.kasp.check_dnssecstatus(ns1, zone, zsks, policy=policy) # - zone is signed - isctest.kasp.zone_is_signed(ns1, zone) + isctest.kasp.check_zone_is_signed(ns1, zone) # - dnssec_verify - isctest.kasp.dnssec_verify(ns1, zone) + isctest.kasp.check_dnssec_verify(ns1, zone) # - check keys check_keys(zsks, lifetime, alg, size, offset=offset, with_state=True) # - check apex @@ -942,9 +942,9 @@ def test_ksr_unlimited(servers): # - check rndc dnssec -status output isctest.kasp.check_dnssecstatus(ns1, zone, zsks, policy=policy) # - zone is signed - isctest.kasp.zone_is_signed(ns1, zone) + isctest.kasp.check_zone_is_signed(ns1, zone) # - dnssec_verify - isctest.kasp.dnssec_verify(ns1, zone) + isctest.kasp.check_dnssec_verify(ns1, zone) # - check keys check_keys(zsks, lifetime, alg, size, with_state=True) # - check apex @@ -1039,9 +1039,9 @@ def test_ksr_twotone(servers): # - check rndc dnssec -status output isctest.kasp.check_dnssecstatus(ns1, zone, zsks, policy=policy) # - zone is signed - isctest.kasp.zone_is_signed(ns1, zone) + isctest.kasp.check_zone_is_signed(ns1, zone) # - dnssec_verify - isctest.kasp.dnssec_verify(ns1, zone) + isctest.kasp.check_dnssec_verify(ns1, zone) # - check keys alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") size = os.environ.get("DEFAULT_BITS") From 00ec96e6e6c05f043404002d3cdc387657933750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Tue, 8 Oct 2024 14:10:50 +0200 Subject: [PATCH 06/12] Remove unused isctest/kasp code (cherry picked from commit b5633462bfe7fc1e5e684680bb33fed5e8267d6d) --- bin/tests/system/isctest/kasp.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py index a2bc24d926..2160ce1e0d 100644 --- a/bin/tests/system/isctest/kasp.py +++ b/bin/tests/system/isctest/kasp.py @@ -26,12 +26,7 @@ import isctest.log DEFAULT_TTL = 300 -def _save_response(response, fname): - with open(fname, "w", encoding="utf-8") as file: - file.write(response.to_text()) - - -def _query(server, qname, qtype, outfile=None): +def _query(server, qname, qtype): query = dns.message.make_query(qname, qtype, use_edns=True, want_dnssec=True) try: response = dns.query.tcp(query, server.ip, port=server.ports.dns, timeout=3) @@ -39,9 +34,6 @@ def _query(server, qname, qtype, outfile=None): isctest.log.debug(f"query timeout for query {qname} {qtype} to {server.ip}") return None - if outfile is not None: - _save_response(response, outfile) - return response From 3cffc34551f2ddfb4d86703a908e356ff09ebd9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Tue, 8 Oct 2024 14:11:56 +0200 Subject: [PATCH 07/12] Simplify command invocation (cherry picked from commit 732a959d9a1e392f32c7365ecf270ea08f585eee) --- bin/tests/system/isctest/kasp.py | 3 +-- bin/tests/system/ksr/tests_ksr.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py index 2160ce1e0d..9676fab92b 100644 --- a/bin/tests/system/isctest/kasp.py +++ b/bin/tests/system/isctest/kasp.py @@ -291,8 +291,7 @@ def check_dnssec_verify(server, zone): file.write(rr.to_text()) file.write("\n") - verify_command = [*os.environ.get("VERIFY").split(), "-z", "-o", zone, zonefile] - + verify_command = [os.environ.get("VERIFY"), "-z", "-o", zone, zonefile] isctest.run.cmd(verify_command) diff --git a/bin/tests/system/ksr/tests_ksr.py b/bin/tests/system/ksr/tests_ksr.py index fa61c3ef87..f8628fb31a 100644 --- a/bin/tests/system/ksr/tests_ksr.py +++ b/bin/tests/system/ksr/tests_ksr.py @@ -49,7 +49,7 @@ def keystr_to_keylist(keystr: str, keydir: Optional[str] = None) -> List[Key]: def keygen(zone, policy, keydir, when="now"): keygen_command = [ - *os.environ.get("KEYGEN").split(), + os.environ.get("KEYGEN"), "-l", "ns1/named.conf", "-fK", @@ -71,7 +71,7 @@ def keygen(zone, policy, keydir, when="now"): def ksr(zone, policy, action, options="", raise_on_exception=True): ksr_command = [ - *os.environ.get("KSR").split(), + os.environ.get("KSR"), "-l", "ns1/named.conf", "-k", From ba2fe0a8306b39e31d81985b43024a2191c90b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Tue, 8 Oct 2024 14:12:10 +0200 Subject: [PATCH 08/12] Use f-strings (cherry picked from commit 55ec9f94bc04eb76090528d939b798048fd7aa70) --- bin/tests/system/isctest/kasp.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py index 9676fab92b..fb752e8dc7 100644 --- a/bin/tests/system/isctest/kasp.py +++ b/bin/tests/system/isctest/kasp.py @@ -301,18 +301,18 @@ def check_dnssecstatus(server, zone, keys, policy=None, view=None): # policy name is returned, and if all expected keys are listed. response = "" if view is None: - response = server.rndc("dnssec -status {}".format(zone), log=False) + response = server.rndc(f"dnssec -status {zone}", log=False) else: - response = server.rndc("dnssec -status {} in {}".format(zone, view), log=False) + response = server.rndc(f"dnssec -status {zone} in {view}", log=False) if policy is None: assert "Zone does not have dnssec-policy" in response return - assert "dnssec-policy: {}".format(policy) in response + assert f"dnssec-policy: {policy}" in response for key in keys: - assert "key: {}".format(key.tag) in response + assert f"key: {key.tag}" in response def _check_signatures(signatures, covers, fqdn, keys): From 36ab7489f7a7c9d04b67904511b4729b1922c160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Tue, 8 Oct 2024 14:33:37 +0200 Subject: [PATCH 09/12] Move algorithm defaults to check_keys() (cherry picked from commit c9ecd2a618dbae1b6234f5a76368a06a238a13eb) --- bin/tests/system/ksr/tests_ksr.py | 47 +++++++++++++------------------ 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/bin/tests/system/ksr/tests_ksr.py b/bin/tests/system/ksr/tests_ksr.py index f8628fb31a..6c945ba29e 100644 --- a/bin/tests/system/ksr/tests_ksr.py +++ b/bin/tests/system/ksr/tests_ksr.py @@ -88,7 +88,14 @@ def ksr(zone, policy, action, options="", raise_on_exception=True): # pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-statements -def check_keys(keys, lifetime, alg, size, offset=0, with_state=False): +def check_keys( + keys, + lifetime, + alg=os.environ["DEFAULT_ALGORITHM_NUMBER"], + size=os.environ["DEFAULT_BITS"], + offset=0, + with_state=False, +): # Check keys that were created. num = 0 @@ -422,10 +429,8 @@ def test_ksr_common(servers): zsks = keystr_to_keylist(out) assert len(zsks) == 2 - alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") - size = os.environ.get("DEFAULT_BITS") lifetime = timedelta(days=31 * 6) - check_keys(zsks, lifetime, alg, size) + check_keys(zsks, lifetime) # check that 'dnssec-ksr keygen' pregenerates right amount of keys # in the given key directory @@ -434,10 +439,8 @@ def test_ksr_common(servers): zsks = keystr_to_keylist(out, zskdir) assert len(zsks) == 2 - alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") - size = os.environ.get("DEFAULT_BITS") lifetime = timedelta(days=31 * 6) - check_keys(zsks, lifetime, alg, size) + check_keys(zsks, lifetime) for key in zsks: privatefile = f"{key.path}.private" @@ -511,7 +514,7 @@ def test_ksr_common(servers): out, _ = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i {now} -e +2y") overlapping_zsks2 = keystr_to_keylist(out, zskdir) assert len(overlapping_zsks2) == 4 - check_keys(overlapping_zsks2, lifetime, alg, size) + check_keys(overlapping_zsks2, lifetime) for index, key in enumerate(overlapping_zsks2): assert overlapping_zsks[index] == key @@ -588,7 +591,7 @@ def test_ksr_common(servers): # - dnssec_verify isctest.kasp.check_dnssec_verify(ns1, zone) # - check keys - check_keys(overlapping_zsks, lifetime, alg, size, with_state=True) + check_keys(overlapping_zsks, lifetime, with_state=True) # - check apex isctest.kasp.check_apex(ns1, zone, ksks, overlapping_zsks) # - check subdomain @@ -616,10 +619,8 @@ def test_ksr_lastbundle(servers): zsks = keystr_to_keylist(out, zskdir) assert len(zsks) == 2 - alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") - size = os.environ.get("DEFAULT_BITS") lifetime = timedelta(days=31 * 6) - check_keys(zsks, lifetime, alg, size, offset=offset) + check_keys(zsks, lifetime, offset=offset) # check that 'dnssec-ksr request' creates correct ksr then = zsks[0].get_timing("Created") + offset @@ -666,7 +667,7 @@ def test_ksr_lastbundle(servers): # - dnssec_verify isctest.kasp.check_dnssec_verify(ns1, zone) # - check keys - check_keys(zsks, lifetime, alg, size, offset=offset, with_state=True) + check_keys(zsks, lifetime, offset=offset, with_state=True) # - check apex isctest.kasp.check_apex(ns1, zone, ksks, zsks) # - check subdomain @@ -698,10 +699,8 @@ def test_ksr_inthemiddle(servers): zsks = keystr_to_keylist(out, zskdir) assert len(zsks) == 4 - alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") - size = os.environ.get("DEFAULT_BITS") lifetime = timedelta(days=31 * 6) - check_keys(zsks, lifetime, alg, size, offset=offset) + check_keys(zsks, lifetime, offset=offset) # check that 'dnssec-ksr request' creates correct ksr then = zsks[0].get_timing("Created") @@ -749,7 +748,7 @@ def test_ksr_inthemiddle(servers): # - dnssec_verify isctest.kasp.check_dnssec_verify(ns1, zone) # - check keys - check_keys(zsks, lifetime, alg, size, offset=offset, with_state=True) + check_keys(zsks, lifetime, offset=offset, with_state=True) # - check apex isctest.kasp.check_apex(ns1, zone, ksks, zsks) # - check subdomain @@ -853,10 +852,8 @@ def test_ksr_unlimited(servers): zsks = keystr_to_keylist(out, zskdir) assert len(zsks) == 1 - alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") - size = os.environ.get("DEFAULT_BITS") lifetime = None - check_keys(zsks, lifetime, alg, size) + check_keys(zsks, lifetime) # check that 'dnssec-ksr request' creates correct ksr now = zsks[0].get_timing("Created") @@ -946,7 +943,7 @@ def test_ksr_unlimited(servers): # - dnssec_verify isctest.kasp.check_dnssec_verify(ns1, zone) # - check keys - check_keys(zsks, lifetime, alg, size, with_state=True) + check_keys(zsks, lifetime, with_state=True) # - check apex isctest.kasp.check_apex(ns1, zone, ksks, zsks) # - check subdomain @@ -988,10 +985,8 @@ def test_ksr_twotone(servers): assert len(zsks_defalg) == 4 assert len(zsks_altalg) == 3 - alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") - size = os.environ.get("DEFAULT_BITS") lifetime = timedelta(days=31 * 3) - check_keys(zsks_defalg, lifetime, alg, size) + check_keys(zsks_defalg, lifetime) alg = os.environ.get("ALTERNATIVE_ALGORITHM_NUMBER") size = os.environ.get("ALTERNATIVE_BITS") @@ -1043,10 +1038,8 @@ def test_ksr_twotone(servers): # - dnssec_verify isctest.kasp.check_dnssec_verify(ns1, zone) # - check keys - alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER") - size = os.environ.get("DEFAULT_BITS") lifetime = timedelta(days=31 * 3) - check_keys(zsks_defalg, lifetime, alg, size, with_state=True) + check_keys(zsks_defalg, lifetime, with_state=True) alg = os.environ.get("ALTERNATIVE_ALGORITHM_NUMBER") size = os.environ.get("ALTERNATIVE_BITS") From fafb75ff8ed9dc9b1abf8909ffa15f10bcebd889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Tue, 8 Oct 2024 15:02:37 +0200 Subject: [PATCH 10/12] Use difflib rather than diff cmd (cherry picked from commit b7207fa3e737fc364469179d6ced8e13d54010e0) --- bin/tests/system/ksr/tests_ksr.py | 42 ++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/bin/tests/system/ksr/tests_ksr.py b/bin/tests/system/ksr/tests_ksr.py index 6c945ba29e..ba02c5282e 100644 --- a/bin/tests/system/ksr/tests_ksr.py +++ b/bin/tests/system/ksr/tests_ksr.py @@ -12,6 +12,7 @@ # pylint: disable=too-many-lines from datetime import timedelta +import difflib import os import shutil import time @@ -33,14 +34,25 @@ def between(value, start, end): return start < value < end -def file_contents_equal(file1, file2): - diff_command = [ - "diff", - "-w", - file1, - file2, - ] - isctest.run.cmd(diff_command) +def check_file_contents_equal(file1, file2): + def normalize_line(line): + # remove trailing&leading whitespace and replace multiple whitespaces + return " ".join(line.split()) + + def read_lines(file_path): + with open(file_path, "r") as file: + return [normalize_line(line) for line in file.readlines()] + + lines1 = read_lines(file1) + lines2 = read_lines(file2) + + differ = difflib.Differ() + diff = differ.compare(lines1, lines2) + + for line in diff: + assert not line.startswith("+ ") and not line.startswith( + "- " + ), f'file contents of "{file1}" and "{file2}" differ' def keystr_to_keylist(keystr: str, keydir: Optional[str] = None) -> List[Key]: @@ -483,9 +495,9 @@ def test_ksr_common(servers): assert len(selected_zsks) == 2 for index, key in enumerate(selected_zsks): assert zsks[index] == key - file_contents_equal(f"{key.path}.private", f"{key.path}.private.backup") - file_contents_equal(f"{key.path}.key", f"{key.path}.key.backup") - file_contents_equal(f"{key.path}.state", f"{key.path}.state.backup") + check_file_contents_equal(f"{key.path}.private", f"{key.path}.private.backup") + check_file_contents_equal(f"{key.path}.key", f"{key.path}.key.backup") + check_file_contents_equal(f"{key.path}.state", f"{key.path}.state.backup") # check that 'dnssec-ksr keygen' generates only necessary keys for # overlapping time bundle @@ -506,9 +518,11 @@ def test_ksr_common(servers): for index, key in enumerate(overlapping_zsks): if index < 2: assert zsks[index] == key - file_contents_equal(f"{key.path}.private", f"{key.path}.private.backup") - file_contents_equal(f"{key.path}.key", f"{key.path}.key.backup") - file_contents_equal(f"{key.path}.state", f"{key.path}.state.backup") + check_file_contents_equal( + f"{key.path}.private", f"{key.path}.private.backup" + ) + check_file_contents_equal(f"{key.path}.key", f"{key.path}.key.backup") + check_file_contents_equal(f"{key.path}.state", f"{key.path}.state.backup") # run 'dnssec-ksr keygen' again with verbosity 0 out, _ = ksr(zone, policy, "keygen", options=f"-K {zskdir} -i {now} -e +2y") From f1fc2e1db47509ec67120c01208abb11cc5057a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Mon, 14 Oct 2024 14:49:38 +0200 Subject: [PATCH 11/12] Address pylint issues (cherry picked from commit 3c6124a0933d95e154ded274cea6fbef5ac60cc8) --- bin/tests/system/isctest/kasp.py | 1 + bin/tests/system/ksr/tests_ksr.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py index fb752e8dc7..a8c25c0d49 100644 --- a/bin/tests/system/isctest/kasp.py +++ b/bin/tests/system/isctest/kasp.py @@ -315,6 +315,7 @@ def check_dnssecstatus(server, zone, keys, policy=None, view=None): assert f"key: {key.tag}" in response +# pylint: disable=too-many-branches,too-many-locals def _check_signatures(signatures, covers, fqdn, keys): now = KeyTimingMetadata.now() numsigs = 0 diff --git a/bin/tests/system/ksr/tests_ksr.py b/bin/tests/system/ksr/tests_ksr.py index ba02c5282e..6caf4e7672 100644 --- a/bin/tests/system/ksr/tests_ksr.py +++ b/bin/tests/system/ksr/tests_ksr.py @@ -18,8 +18,6 @@ import shutil import time from typing import List, Optional -from datetime import datetime - import isctest from isctest.kasp import ( Key, @@ -40,7 +38,7 @@ def check_file_contents_equal(file1, file2): return " ".join(line.split()) def read_lines(file_path): - with open(file_path, "r") as file: + with open(file_path, "r", encoding="utf-8") as file: return [normalize_line(line) for line in file.readlines()] lines1 = read_lines(file1) From b46ddad5df8cd0c001b937887c5b77219b6bd383 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Tue, 15 Oct 2024 10:43:41 +0200 Subject: [PATCH 12/12] Retry dnssec-verify in kasp test code It is possible that the zone is not yet fully signed because it is signed in batches. Retry the AXFR and verify command a couple of times. (cherry picked from commit b8b3df0676dedbca6e94340825a173ed1764f5b5) --- bin/tests/system/isctest/kasp.py | 46 +++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py index a8c25c0d49..b459b15b3e 100644 --- a/bin/tests/system/isctest/kasp.py +++ b/bin/tests/system/isctest/kasp.py @@ -13,6 +13,7 @@ from functools import total_ordering import os from pathlib import Path import re +import subprocess import time from typing import Optional, Union @@ -21,7 +22,7 @@ from datetime import timedelta import dns import isctest.log - +import isctest.query DEFAULT_TTL = 300 @@ -29,7 +30,7 @@ DEFAULT_TTL = 300 def _query(server, qname, qtype): query = dns.message.make_query(qname, qtype, use_edns=True, want_dnssec=True) try: - response = dns.query.tcp(query, server.ip, port=server.ports.dns, timeout=3) + response = isctest.query.tcp(query, server.ip, server.ports.dns, timeout=3) except dns.exception.Timeout: isctest.log.debug(f"query timeout for query {qname} {qtype} to {server.ip}") return None @@ -278,21 +279,34 @@ def check_zone_is_signed(server, zone): def check_dnssec_verify(server, zone): # Check if zone if DNSSEC valid with dnssec-verify. fqdn = f"{zone}." - transfer = _query(server, fqdn, dns.rdatatype.AXFR) - if not isinstance(transfer, dns.message.Message): - isctest.log.debug(f"no response for {fqdn} AXFR from {server.ip}") - elif transfer.rcode() != dns.rcode.NOERROR: - rcode = dns.rcode.to_text(transfer.rcode()) - isctest.log.debug(f"{rcode} response for {fqdn} AXFR from {server.ip}") - else: - zonefile = f"{zone}.axfr" - with open(zonefile, "w", encoding="utf-8") as file: - for rr in transfer.answer: - file.write(rr.to_text()) - file.write("\n") - verify_command = [os.environ.get("VERIFY"), "-z", "-o", zone, zonefile] - isctest.run.cmd(verify_command) + verified = False + for _ in range(10): + transfer = _query(server, fqdn, dns.rdatatype.AXFR) + if not isinstance(transfer, dns.message.Message): + isctest.log.debug(f"no response for {fqdn} AXFR from {server.ip}") + elif transfer.rcode() != dns.rcode.NOERROR: + rcode = dns.rcode.to_text(transfer.rcode()) + isctest.log.debug(f"{rcode} response for {fqdn} AXFR from {server.ip}") + else: + zonefile = f"{zone}.axfr" + with open(zonefile, "w", encoding="utf-8") as file: + for rr in transfer.answer: + file.write(rr.to_text()) + file.write("\n") + + try: + verify_command = [os.environ.get("VERIFY"), "-z", "-o", zone, zonefile] + verified = isctest.run.cmd(verify_command) + except subprocess.CalledProcessError: + pass + + if verified: + break + + time.sleep(1) + + assert verified def check_dnssecstatus(server, zone, keys, policy=None, view=None):