From ea1fc5c47b1e242eaa43483a00bbb5922c4ad2d3 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. --- 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 ea4d3e729e..9919fd4210 100644 --- a/bin/dnssec/dnssec-ksr.c +++ b/bin/dnssec/dnssec-ksr.c @@ -182,10 +182,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); @@ -220,7 +224,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 a3829990fdb67b646f41ef69d2d4ab3b5debbc24 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 --- 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 a15bf6704beddc7ef18609094e8f5c7505b3e3e2 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. --- 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 2b0a8fcfb5084b23477f1c66b9f32445422a4461 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 --- 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 67957d1f54e01bc3066effc0fa5b6486efb66178 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_". --- 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 b5633462bfe7fc1e5e684680bb33fed5e8267d6d 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 --- 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 732a959d9a1e392f32c7365ecf270ea08f585eee 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 --- 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 55ec9f94bc04eb76090528d939b798048fd7aa70 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 --- 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 c9ecd2a618dbae1b6234f5a76368a06a238a13eb 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() --- 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 b7207fa3e737fc364469179d6ced8e13d54010e0 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 --- 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 3c6124a0933d95e154ded274cea6fbef5ac60cc8 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 --- 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 b8b3df0676dedbca6e94340825a173ed1764f5b5 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. --- 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):