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/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..b459b15b3e --- /dev/null +++ b/bin/tests/system/isctest/kasp.py @@ -0,0 +1,573 @@ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# SPDX-License-Identifier: MPL-2.0 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at https://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. + +from functools import total_ordering +import os +from pathlib import Path +import re +import subprocess +import time +from typing import Optional, Union + +from datetime import datetime +from datetime import timedelta + +import dns +import isctest.log +import isctest.query + +DEFAULT_TTL = 300 + + +def _query(server, qname, qtype): + query = dns.message.make_query(qname, qtype, use_edns=True, want_dnssec=True) + try: + 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 + + return response + + +@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 + + +@total_ordering +class Key: + """ + Represent a key from a keyfile. + + 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. + """ + + 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:]) + + 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 + + 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 + + def is_ksk(self) -> bool: + return self.get_metadata("KSK") == "yes" + + def is_zsk(self) -> bool: + return self.get_metadata("ZSK") == "yes" + + def dnskey_equals(self, value, cdnskey=False): + 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(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:]) + + return pubkey_fromfile == pubkey_fromwire + + def cds_equals(self, value, alg): + cds = value.split() + + dsfromkey_command = [ + os.environ.get("DSFROMKEY"), + "-T", + "3600", + "-a", + alg, + "-C", + "-w", + str(self.keyfile), + ] + + 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 + + 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 + + return digest_fromfile == digest_fromwire + + def __lt__(self, other: "Key"): + return self.name < other.name + + def __eq__(self, other: object): + return isinstance(other, Key) and self.path == other.path + + def __repr__(self): + return self.path + + +def check_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 check_dnssec_verify(server, zone): + # Check if zone if DNSSEC valid with dnssec-verify. + fqdn = f"{zone}." + + 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): + # 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(f"dnssec -status {zone}", log=False) + else: + 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 f"dnssec-policy: {policy}" in response + + for key in keys: + 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 + zrrsig = True + if covers in [dns.rdatatype.DNSKEY, dns.rdatatype.CDNSKEY, dns.rdatatype.CDS]: + zrrsig = False + krrsig = not zrrsig + + for key in keys: + activate = key.get_timing("Activate") + inactive = key.get_timing("Inactive", must_exist=False) + + 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"{key.tag} {fqdn}" not in rrsig + continue + + if zrrsig and key.is_zsk(): + has_rrsig = False + for rrsig in signatures: + if f"{key.tag} {fqdn}" in rrsig: + has_rrsig = True + break + assert has_rrsig + numsigs += 1 + + if zrrsig and not key.is_zsk(): + for rrsig in signatures: + assert f"{key.tag} {fqdn}" not in rrsig + + if krrsig and key.is_ksk(): + has_rrsig = False + for rrsig in signatures: + if f"{key.tag} {fqdn}" in rrsig: + has_rrsig = True + break + assert has_rrsig + numsigs += 1 + + if krrsig and not key.is_ksk(): + for rrsig in signatures: + assert f"{key.tag} {fqdn}" not in rrsig + + return numsigs + + +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. + 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) + numsigs += _check_signatures(signatures, covers, fqdn, zsks) + + assert numsigs == len(signatures) + + +def _check_dnskeys(dnskeys, keys, cdnskey=False): + now = KeyTimingMetadata.now() + 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 = 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 key.dnskey_equals(dnskey, cdnskey=cdnskey) + continue + + has_dnskey = False + for dnskey in dnskeys: + if key.dnskey_equals(dnskey, cdnskey=cdnskey): + has_dnskey = True + break + + assert has_dnskey + numkeys += 1 + + return numkeys + + +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 + # 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, cdnskey=cdnskey) + if not cdnskey: + numkeys += _check_dnskeys(dnskeys, zsks) + + assert numkeys == len(dnskeys) + + +# pylint: disable=too-many-locals +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 = KeyTimingMetadata.now() + 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: + assert key.is_ksk() + + 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 key.cds_equals(cds, "SHA-256") + continue + + has_cds = False + for cds in cdss: + if key.cds_equals(cds, "SHA-256"): + 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 + + +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}." + + # test dnskey query + dnskeys, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.DNSKEY) + assert len(dnskeys) > 0 + check_dnskeys(dnskeys, ksks, zsks) + assert len(rrsigs) > 0 + 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) + + # test cdnskey query + cdnskeys, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.CDNSKEY) + assert len(cdnskeys) > 0 + check_dnskeys(cdnskeys, ksks, zsks, cdnskey=True) + assert len(rrsigs) > 0 + 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) + assert len(rrsigs) > 0 + check_signatures(rrsigs, dns.rdatatype.CDS, fqdn, ksks, zsks) + + +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}." + 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) 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/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@; }; }; 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..6caf4e7672 --- /dev/null +++ b/bin/tests/system/ksr/tests_ksr.py @@ -0,0 +1,1063 @@ +# 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 + +from datetime import timedelta +import difflib +import os +import shutil +import time +from typing import List, Optional + +import isctest +from isctest.kasp import ( + Key, + KeyTimingMetadata, +) + + +def between(value, start, end): + if value is None or start is None or end is None: + return False + + return start < value < end + + +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", encoding="utf-8") 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]: + return [Key(name, keydir) for name in keystr.split()] + + +def keygen(zone, policy, keydir, when="now"): + keygen_command = [ + os.environ.get("KEYGEN"), + "-l", + "ns1/named.conf", + "-fK", + "-K", + keydir, + "-k", + policy, + "-P", + when, + "-A", + when, + "-P", + "sync", + when, + zone, + ] + return isctest.run.cmd(keygen_command, log_stdout=True).stdout.decode("utf-8") + + +def ksr(zone, policy, action, options="", raise_on_exception=True): + ksr_command = [ + os.environ.get("KSR"), + "-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=os.environ["DEFAULT_ALGORITHM_NUMBER"], + size=os.environ["DEFAULT_BITS"], + offset=0, + with_state=False, +): + # Check keys that were created. + num = 0 + + now = KeyTimingMetadata.now() + + for key in keys: + # created: from keyfile plus offset + created = key.get_timing("Created") + offset + + # active: retired previous key + if num == 0: + active = created + else: + active = retired + + # published: dnskey-ttl + publish-safety + propagation + published = active - timedelta(hours=2, minutes=5) + + # retired: zsk-lifetime + 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 = None + removed = None + + if retired is None or between(now, published, retired): + goal = "omnipresent" + pubdelay = published + timedelta(hours=2, minutes=5) + signdelay = active + timedelta(days=10, hours=1, minutes=5) + + 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(key.statefile, "r", encoding="utf-8") as file: + metadata = file.read() + assert f"Algorithm: {alg}" in metadata + assert f"Length: {size}" 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 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 + + 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 + + num += 1 + + +def check_keysigningrequest(out, zsks, start, end): + lines = out.split("\n") + line_no = 0 + + inception = start + 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 = key.get_timing("Publish") + if between(published, inception, next_bundle): + next_bundle = published + + removed = key.get_timing("Delete", must_exist=False) + if between(removed, inception, next_bundle): + next_bundle = removed + + if published > inception: + continue + if removed is not None and inception >= removed: + continue + + # this zsk must be in the ksr + assert key.dnskey_equals(lines[line_no]) + 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, + cdnskey=True, + cds="SHA-256", +): + lines = out.split("\n") + line_no = 0 + next_bundle = end + 1 + + inception = start + while inception < 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 = inception - timedelta(hours=1) # clockskew + sigend = inception + timedelta(days=14) # sig-validity + next_bundle = 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 = key.get_timing("Publish") + removed = key.get_timing("Delete", must_exist=False) + + if published > inception: + continue + if removed is not None and inception >= removed: + continue + + # this ksk must be in the ksr + assert key.dnskey_equals(lines[line_no]) + line_no += 1 + + # expect zsks + for key in sorted(zsks): + published = key.get_timing("Publish") + if between(published, inception, next_bundle): + next_bundle = published + + removed = key.get_timing("Delete", must_exist=False) + if between(removed, inception, next_bundle): + next_bundle = removed + + if published > inception: + continue + if removed is not None and inception >= removed: + continue + + # this zsk must be in the ksr + assert key.dnskey_equals(lines[line_no]) + line_no += 1 + + # expect rrsig(dnskey) + for key in sorted(ksks): + active = key.get_timing("Activate") + inactive = key.get_timing("Inactive", must_exist=False) + if active > inception: + continue + if inactive is not None and inception >= inactive: + continue + + # there must be a signature of this ksk + 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 + + # expect cdnskey + if cdnskey: + for key in sorted(ksks): + published = key.get_timing("Publish") + removed = key.get_timing("Delete", must_exist=False) + if published > inception: + continue + if removed is not None and inception >= removed: + continue + + # the cdnskey of this ksk must be in the ksr + assert key.dnskey_equals(lines[line_no], cdnskey=True) + line_no += 1 + + # expect rrsig(cdnskey) + for key in sorted(ksks): + active = key.get_timing("Activate") + inactive = key.get_timing("Inactive", must_exist=False) + if active > inception: + continue + if inactive is not None and inception >= inactive: + continue + + # there must be a signature of this ksk + 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 + + # expect cds + if cds != "": + for key in sorted(ksks): + published = key.get_timing("Publish") + removed = key.get_timing("Delete", must_exist=False) + if published > inception: + continue + 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 key.cds_equals(lines[line_no], alg.strip()) + line_no += 1 + + # expect rrsig(cds) + for key in sorted(ksks): + active = key.get_timing("Activate") + inactive = key.get_timing("Inactive", must_exist=False) + if active > inception: + continue + if inactive is not None and inception >= inactive: + continue + + # there must be a signature of this ksk + 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 + + 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 + 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 = keystr_to_keylist(out) + assert len(zsks) == 2 + + lifetime = timedelta(days=31 * 6) + check_keys(zsks, lifetime) + + # check that 'dnssec-ksr keygen' pregenerates right amount of keys + # in the given key directory + 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 + + lifetime = timedelta(days=31 * 6) + check_keys(zsks, lifetime) + + for key in zsks: + 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 = 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) + + # check that 'dnssec-ksr sign' creates correct skr + out, _ = ksr( + zone, policy, "sign", options=f"-K {kskdir} -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) + + # 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 {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 + 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 + 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() + 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 + 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") + overlapping_zsks2 = keystr_to_keylist(out, zskdir) + assert len(overlapping_zsks2) == 4 + check_keys(overlapping_zsks2, lifetime) + 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) + + # 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 = 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( + 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, + ) + + # 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.check_zone_is_signed(ns1, zone) + # - dnssec_verify + isctest.kasp.check_dnssec_verify(ns1, zone) + # - check keys + check_keys(overlapping_zsks, lifetime, with_state=True) + # - check apex + isctest.kasp.check_apex(ns1, zone, ksks, overlapping_zsks) + # - check subdomain + isctest.kasp.check_subdomain(ns1, zone, ksks, overlapping_zsks) + + +# pylint: disable=too-many-locals +def test_ksr_lastbundle(servers): + zone = "last-bundle.test" + policy = "common" + n = 1 + + # create ksk + 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 + 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 + + lifetime = timedelta(days=31 * 6) + check_keys(zsks, lifetime, offset=offset) + + # check that 'dnssec-ksr request' creates correct ksr + 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) + + # check that 'dnssec-ksr sign' creates correct skr + out, _ = ksr( + zone, policy, "sign", options=f"-K {kskdir} -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) + + # 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.check_zone_is_signed(ns1, zone) + # - dnssec_verify + isctest.kasp.check_dnssec_verify(ns1, zone) + # - check keys + check_keys(zsks, lifetime, offset=offset, with_state=True) + # - check apex + isctest.kasp.check_apex(ns1, zone, ksks, zsks) + # - check subdomain + 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" + 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 + 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 + 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 + + lifetime = timedelta(days=31 * 6) + check_keys(zsks, lifetime, offset=offset) + + # check that 'dnssec-ksr request' creates correct ksr + 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) + + # check that 'dnssec-ksr sign' creates correct skr + out, _ = ksr( + zone, policy, "sign", options=f"-K {kskdir} -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) + + # 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.check_zone_is_signed(ns1, zone) + # - dnssec_verify + isctest.kasp.check_dnssec_verify(ns1, zone) + # - check keys + check_keys(zsks, lifetime, offset=offset, with_state=True) + # - check apex + isctest.kasp.check_apex(ns1, zone, ksks, zsks) + # - check subdomain + 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" + 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 + 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 + 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 = 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: + file.write(out) + + # sign request + out, _ = ksr( + zone, policy, "sign", options=f"-K {kskdir} -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 + 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 + 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 + + lifetime = None + check_keys(zsks, lifetime) + + # check that 'dnssec-ksr request' creates correct ksr + 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) + + # check that 'dnssec-ksr sign' creates correct skr without cdnskey + out, _ = ksr( + zone, "no-cdnskey", "sign", options=f"-K {kskdir} -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, + 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 {kskdir} -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, + cds="", + ) + + # check that 'dnssec-ksr sign' creates correct skr + out, _ = ksr( + zone, policy, "sign", options=f"-K {kskdir} -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) + + # 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.check_zone_is_signed(ns1, zone) + # - dnssec_verify + isctest.kasp.check_dnssec_verify(ns1, zone) + # - check keys + check_keys(zsks, lifetime, with_state=True) + # - check apex + isctest.kasp.check_apex(ns1, zone, ksks, zsks) + # - check subdomain + isctest.kasp.check_subdomain(ns1, zone, ksks, zsks) + + +# pylint: disable=too-many-locals +def test_ksr_twotone(servers): + zone = "two-tone.test" + policy = "two-tone" + n = 1 + + # create ksk + 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 + 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 + # 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 = zsk.get_metadata("Algorithm") + 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 + + lifetime = timedelta(days=31 * 3) + check_keys(zsks_defalg, lifetime) + + alg = os.environ.get("ALTERNATIVE_ALGORITHM_NUMBER") + size = os.environ.get("ALTERNATIVE_BITS") + lifetime = timedelta(days=31 * 5) + check_keys(zsks_altalg, lifetime, alg, size) + + # check that 'dnssec-ksr request' creates correct ksr + 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) + + # check that 'dnssec-ksr sign' creates correct skr + out, _ = ksr( + 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 = -timedelta(days=5) + check_signedkeyresponse(out, zone, ksks, zsks, now, until, refresh) + + # 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.check_zone_is_signed(ns1, zone) + # - dnssec_verify + isctest.kasp.check_dnssec_verify(ns1, zone) + # - check keys + lifetime = timedelta(days=31 * 3) + check_keys(zsks_defalg, lifetime, with_state=True) + + alg = os.environ.get("ALTERNATIVE_ALGORITHM_NUMBER") + size = os.environ.get("ALTERNATIVE_BITS") + 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) + # - check subdomain + isctest.kasp.check_subdomain(ns1, zone, ksks, zsks) 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()