diff --git a/bin/dnssec/dnssec-keyfromlabel.c b/bin/dnssec/dnssec-keyfromlabel.c index 35dacc3f31..a52980f71f 100644 --- a/bin/dnssec/dnssec-keyfromlabel.c +++ b/bin/dnssec/dnssec-keyfromlabel.c @@ -43,6 +43,8 @@ const char *program = "dnssec-keyfromlabel"; +static uint16_t tag_min = 0, tag_max = 0xffff; + ISC_NORETURN static void usage(void); @@ -68,6 +70,7 @@ usage(void) { "key files\n"); fprintf(stderr, " -k: generate a TYPE=KEY key\n"); fprintf(stderr, " -L ttl: default key TTL\n"); + fprintf(stderr, " -M :: allowed Key ID range\n"); fprintf(stderr, " -n nametype: ZONE | HOST | ENTITY | USER | " "OTHER\n"); fprintf(stderr, " (DNSKEY generation defaults to ZONE\n"); @@ -156,7 +159,7 @@ main(int argc, char **argv) { isc_commandline_errprint = false; -#define CMDLINE_FLAGS "3A:a:Cc:D:E:Ff:GhI:i:kK:L:l:n:P:p:R:S:t:v:Vy" +#define CMDLINE_FLAGS "3A:a:Cc:D:E:Ff:GhI:i:kK:L:l:M:n:P:p:R:S:t:v:Vy" while ((ch = isc_commandline_parse(argc, argv, CMDLINE_FLAGS)) != -1) { switch (ch) { case '3': @@ -203,6 +206,20 @@ main(int argc, char **argv) { case 'l': label = isc_mem_strdup(mctx, isc_commandline_argument); break; + case 'M': { + unsigned long ul; + tag_min = ul = strtoul(isc_commandline_argument, &endp, + 10); + if (*endp != ':' || ul > 0xffff) { + fatal("-M range invalid"); + } + tag_max = ul = strtoul(endp + 1, &endp, 10); + if (*endp != '\0' || ul > 0xffff || tag_max <= tag_min) + { + fatal("-M range invalid"); + } + break; + } case 'n': nametype = isc_commandline_argument; break; @@ -677,7 +694,8 @@ main(int argc, char **argv) { * is a risk of ID collision due to this key or another key * being revoked. */ - if (key_collision(key, name, directory, mctx, &exact)) { + if (key_collision(key, name, directory, mctx, tag_min, tag_max, &exact)) + { isc_buffer_clear(&buf); ret = dst_key_buildfilename(key, 0, directory, &buf); if (ret != ISC_R_SUCCESS) { diff --git a/bin/dnssec/dnssec-keyfromlabel.rst b/bin/dnssec/dnssec-keyfromlabel.rst index d0cc5ef5cd..2805bc612e 100644 --- a/bin/dnssec/dnssec-keyfromlabel.rst +++ b/bin/dnssec/dnssec-keyfromlabel.rst @@ -21,7 +21,7 @@ dnssec-keyfromlabel - DNSSEC key generation tool Synopsis ~~~~~~~~ -:program:`dnssec-keyfromlabel` {**-l** label} [**-3**] [**-a** algorithm] [**-A** date/offset] [**-c** class] [**-D** date/offset] [**-D** sync date/offset] [**-f** flag] [**-G**] [**-I** date/offset] [**-i** interval] [**-k**] [**-K** directory] [**-L** ttl] [**-n** nametype] [**-P** date/offset] [**-P** sync date/offset] [**-p** protocol] [**-R** date/offset] [**-S** key] [**-t** type] [**-v** level] [**-V**] [**-y**] {name} +:program:`dnssec-keyfromlabel` {**-l** label} [**-3**] [**-a** algorithm] [**-A** date/offset] [**-c** class] [**-D** date/offset] [**-D** sync date/offset] [**-f** flag] [**-G**] [**-I** date/offset] [**-i** interval] [**-k**] [**-K** directory] [**-L** ttl] [**-M** tag_min:tag_max] [**-n** nametype] [**-P** date/offset] [**-P** sync date/offset] [**-p** protocol] [**-R** date/offset] [**-S** key] [**-t** type] [**-v** level] [**-V**] [**-y**] {name} Description ~~~~~~~~~~~ @@ -123,6 +123,18 @@ Options place, in which case the existing TTL would take precedence. Setting the default TTL to ``0`` or ``none`` removes it. +.. option:: -M tag_min:tag_max + + This option sets the range of key tag values + that ``dnssec-keyfromlabel`` will accept. If the key tag of the new + key or the key tag of the revoked version of the new key is + outside this range, the new key will be rejected. This is + designed to be used when generating keys in a multi-signer + scenario, where each operator is given a range of key tags to + prevent collisions among different operators. The valid + values for ``tag_min`` and ``tag_max`` are [0..65535]. The + default allows all key tag values to be accepted. + .. option:: -p protocol This option sets the protocol value for the key. The protocol is a number between diff --git a/bin/dnssec/dnssec-keygen.c b/bin/dnssec/dnssec-keygen.c index 5522662994..2bf382179b 100644 --- a/bin/dnssec/dnssec-keygen.c +++ b/bin/dnssec/dnssec-keygen.c @@ -89,6 +89,8 @@ struct keygen_ctx { char *type; int protocol; int size; + uint16_t tag_min; + uint16_t tag_max; int signatory; dns_rdataclass_t rdclass; int options; @@ -177,6 +179,7 @@ usage(void) { fprintf(stderr, " -f : ZSK | KSK | REVOKE\n"); fprintf(stderr, " -F: FIPS mode\n"); fprintf(stderr, " -L : default key TTL\n"); + fprintf(stderr, " -M :: allowed Key ID range\n"); fprintf(stderr, " -p : (default: 3 [dnssec])\n"); fprintf(stderr, " -s : strength value this key signs DNS " "records with (default: 0)\n"); @@ -753,7 +756,9 @@ keygen(keygen_ctx_t *ctx, isc_mem_t *mctx, int argc, char **argv) { * if there is a risk of ID collision due to this key * or another key being revoked. */ - if (key_collision(key, name, ctx->directory, mctx, NULL)) { + if (key_collision(key, name, ctx->directory, mctx, ctx->tag_min, + ctx->tag_max, NULL)) + { conflict = true; if (null_key) { dst_key_free(&key); @@ -862,8 +867,8 @@ main(int argc, char **argv) { /* * Process memory debugging argument first. */ -#define CMDLINE_FLAGS \ - "3A:a:b:Cc:D:d:E:Ff:GhI:i:K:k:L:l:m:n:P:p:qR:r:S:s:" \ +#define CMDLINE_FLAGS \ + "3A:a:b:Cc:D:d:E:Ff:GhI:i:K:k:L:l:M:m:n:P:p:qR:r:S:s:" \ "T:t:v:V" while ((ch = isc_commandline_parse(argc, argv, CMDLINE_FLAGS)) != -1) { switch (ch) { @@ -952,6 +957,21 @@ main(int argc, char **argv) { case 'n': ctx.nametype = isc_commandline_argument; break; + case 'M': { + unsigned long ul; + ctx.tag_min = ul = strtoul(isc_commandline_argument, + &endp, 10); + if (*endp != ':' || ul > 0xffff) { + fatal("-M range invalid"); + } + ctx.tag_max = ul = strtoul(endp + 1, &endp, 10); + if (*endp != '\0' || ul > 0xffff || + ctx.tag_max <= ctx.tag_min) + { + fatal("-M range invalid"); + } + break; + } case 'm': break; case 'p': @@ -1215,6 +1235,8 @@ main(int argc, char **argv) { ctx.ksk = true; ctx.zsk = true; ctx.lifetime = 0; + ctx.tag_min = 0; + ctx.tag_max = 0xffff; keygen(&ctx, mctx, argc, argv); } else { @@ -1263,6 +1285,8 @@ main(int argc, char **argv) { if (ctx.keystore != NULL) { check_keystore_options(&ctx); } + ctx.tag_min = dns_kasp_key_tagmin(kaspkey); + ctx.tag_max = dns_kasp_key_tagmax(kaspkey); if ((ctx.ksk && !ctx.wantksk && ctx.wantzsk) || (ctx.zsk && !ctx.wantzsk && ctx.wantksk)) { diff --git a/bin/dnssec/dnssec-keygen.rst b/bin/dnssec/dnssec-keygen.rst index 88044ee26b..ff73377ecb 100644 --- a/bin/dnssec/dnssec-keygen.rst +++ b/bin/dnssec/dnssec-keygen.rst @@ -21,7 +21,7 @@ dnssec-keygen: DNSSEC key generation tool Synopsis ~~~~~~~~ -:program:`dnssec-keygen` [**-3**] [**-A** date/offset] [**-a** algorithm] [**-b** keysize] [**-C**] [**-c** class] [**-D** date/offset] [**-d** bits] [**-D** sync date/offset] [**-f** flag] [**-F**] [**-G**] [**-h**] [**-I** date/offset] [**-i** interval] [**-K** directory] [**-k** policy] [**-L** ttl] [**-l** file] [**-n** nametype] [**-P** date/offset] [**-P** sync date/offset] [**-p** protocol] [**-q**] [**-R** date/offset] [**-S** key] [**-s** strength] [**-T** rrtype] [**-t** type] [**-V**] [**-v** level] {name} +:program:`dnssec-keygen` [**-3**] [**-A** date/offset] [**-a** algorithm] [**-b** keysize] [**-C**] [**-c** class] [**-D** date/offset] [**-d** bits] [**-D** sync date/offset] [**-f** flag] [**-F**] [**-G**] [**-h**] [**-I** date/offset] [**-i** interval] [**-K** directory] [**-k** policy] [**-L** ttl] [**-l** file] [**-M** tag_min:tag_max] [**-n** nametype] [**-P** date/offset] [**-P** sync date/offset] [**-p** protocol] [**-q**] [**-R** date/offset] [**-S** key] [**-s** strength] [**-T** rrtype] [**-t** type] [**-V**] [**-v** level] {name} Description ~~~~~~~~~~~ @@ -150,6 +150,19 @@ Options This option provides a configuration file that contains a ``dnssec-policy`` statement (matching the policy set with :option:`-k`). +.. option:: -M tag_min:tag_max + + This option sets the range of acceptable key tag values that ``dnssec-keygen`` + will produce. If the key tag of the new key or the key tag of + the revoked version of the new key is outside this range, + the new key will be rejected and another new key will be generated. + This is designed to be used when generating keys in a multi-signer + scenario, where each operator is given a range of key tags to + prevent collisions among different operators. The valid values + for ``tag_min`` and ``tag_max`` are [0..65535]. The default allows all + key tag values to be produced. This option is ignored when ``-k policy`` + is specified. + .. option:: -n nametype This option specifies the owner type of the key. The value of ``nametype`` must diff --git a/bin/dnssec/dnssec-ksr.c b/bin/dnssec/dnssec-ksr.c index aa4522deba..ea4d3e729e 100644 --- a/bin/dnssec/dnssec-ksr.c +++ b/bin/dnssec/dnssec-ksr.c @@ -436,7 +436,10 @@ create_zsk(ksr_ctx_t *ksr, dns_kasp_key_t *kaspkey, dns_dnsseckeylist_t *keys, } /* Do not overwrite an existing key. */ - if (key_collision(key, name, ksr->keydir, mctx, NULL)) { + if (key_collision(key, name, ksr->keydir, mctx, + dns_kasp_key_tagmin(kaspkey), + dns_kasp_key_tagmax(kaspkey), NULL)) + { conflict = true; if (verbose > 0) { isc_buffer_clear(&buf); diff --git a/bin/dnssec/dnssectool.c b/bin/dnssec/dnssectool.c index 0d1163f8c6..820878eaaf 100644 --- a/bin/dnssec/dnssectool.c +++ b/bin/dnssec/dnssectool.c @@ -450,7 +450,7 @@ set_keyversion(dst_key_t *key) { bool key_collision(dst_key_t *dstkey, dns_name_t *name, const char *dir, - isc_mem_t *mctx, bool *exact) { + isc_mem_t *mctx, uint16_t min, uint16_t max, bool *exact) { isc_result_t result; bool conflict = false; dns_dnsseckeylist_t matchkeys; @@ -468,6 +468,21 @@ key_collision(dst_key_t *dstkey, dns_name_t *name, const char *dir, rid = dst_key_rid(dstkey); alg = dst_key_alg(dstkey); + if (min != max) { + if (id < min || id > max) { + fprintf(stderr, "Key ID %d outside of [%u..%u]\n", id, + min, max); + return (true); + } + if (rid < min || rid > max) { + fprintf(stderr, + "Revoked Key ID %d (for tag %d) outside of " + "[%u..%u]\n", + rid, id, min, max); + return (true); + } + } + ISC_LIST_INIT(matchkeys); result = dns_dnssec_findmatchingkeys(name, NULL, dir, NULL, now, mctx, &matchkeys); diff --git a/bin/dnssec/dnssectool.h b/bin/dnssec/dnssectool.h index 3cd0571fd5..dc6a5dfe75 100644 --- a/bin/dnssec/dnssectool.h +++ b/bin/dnssec/dnssectool.h @@ -106,7 +106,7 @@ set_keyversion(dst_key_t *key); bool key_collision(dst_key_t *key, dns_name_t *name, const char *dir, - isc_mem_t *mctx, bool *exact); + isc_mem_t *mctx, uint16_t min, uint16_t max, bool *exact); bool isoptarg(const char *arg, char **argv, void (*usage)(void)); diff --git a/bin/tests/system/checkconf/bad-dnssec-policy-range1.conf b/bin/tests/system/checkconf/bad-dnssec-policy-range1.conf new file mode 100644 index 0000000000..a4398438d5 --- /dev/null +++ b/bin/tests/system/checkconf/bad-dnssec-policy-range1.conf @@ -0,0 +1,18 @@ +/* + * 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. + */ + +dnssec-policy reverse-order { + keys { + csk lifetime unlimited algorithm rsasha256 tag-range 32767 0 2048; + }; +}; diff --git a/bin/tests/system/checkconf/bad-dnssec-policy-range2.conf b/bin/tests/system/checkconf/bad-dnssec-policy-range2.conf new file mode 100644 index 0000000000..f2d2911662 --- /dev/null +++ b/bin/tests/system/checkconf/bad-dnssec-policy-range2.conf @@ -0,0 +1,18 @@ +/* + * 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. + */ + +dnssec-policy too-big-start { + keys { + csk lifetime unlimited algorithm rsasha256 tag-range 65536 0 2048; + }; +}; diff --git a/bin/tests/system/checkconf/bad-dnssec-policy-range3.conf b/bin/tests/system/checkconf/bad-dnssec-policy-range3.conf new file mode 100644 index 0000000000..131a98d1a3 --- /dev/null +++ b/bin/tests/system/checkconf/bad-dnssec-policy-range3.conf @@ -0,0 +1,18 @@ +/* + * 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. + */ + +dnssec-policy too-big-end { + keys { + csk lifetime unlimited algorithm rsasha256 tag-range 0 65536 2048; + }; +}; diff --git a/bin/tests/system/checkconf/bad-dnssec-policy-range4.conf b/bin/tests/system/checkconf/bad-dnssec-policy-range4.conf new file mode 100644 index 0000000000..2b3232ca7a --- /dev/null +++ b/bin/tests/system/checkconf/bad-dnssec-policy-range4.conf @@ -0,0 +1,18 @@ +/* + * 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. + */ + +dnssec-policy start-equals-end { + keys { + csk lifetime unlimited algorithm rsasha256 tag-range 0 0 2048; + }; +}; diff --git a/bin/tests/system/checkconf/good-dnssec-policy-range.conf b/bin/tests/system/checkconf/good-dnssec-policy-range.conf new file mode 100644 index 0000000000..68f9afdd0b --- /dev/null +++ b/bin/tests/system/checkconf/good-dnssec-policy-range.conf @@ -0,0 +1,26 @@ +/* + * 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. + */ + +dnssec-policy restricted-range { + keys { + ksk lifetime unlimited algorithm rsasha256 tag-range 0 32767 2048; + zsk lifetime unlimited algorithm rsasha256 tag-range 0 32767; + }; +}; + +dnssec-policy unrestricted-range { + keys { + ksk lifetime unlimited algorithm rsasha256 2048; + zsk lifetime unlimited algorithm rsasha256; + }; +}; diff --git a/bin/tests/system/checkconf/good.conf.in b/bin/tests/system/checkconf/good.conf.in index e326f7ce74..f7e8cc8415 100644 --- a/bin/tests/system/checkconf/good.conf.in +++ b/bin/tests/system/checkconf/good.conf.in @@ -23,7 +23,7 @@ dnssec-policy "test" { }; dnskey-ttl 3600; keys { - ksk key-directory lifetime P1Y algorithm 13; + ksk key-directory lifetime P1Y algorithm 13 tag-range 0 32767; zsk lifetime P30D algorithm 13; csk key-store "hsm" lifetime P30D algorithm 8 2048; }; diff --git a/bin/tests/system/dnssec/tests.sh b/bin/tests/system/dnssec/tests.sh index 041443271c..f2b7b76685 100644 --- a/bin/tests/system/dnssec/tests.sh +++ b/bin/tests/system/dnssec/tests.sh @@ -4468,5 +4468,24 @@ n=$((n + 1)) if [ "$ret" -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) +echo_i "check that dnssec-keygen honours key tag ranges ($n)" +ret=0 +zone=settagrange +ksk=$("$KEYGEN" -f KSK -q -a $DEFAULT_ALGORITHM -n zone -M 0:32767 "$zone") +zsk=$("$KEYGEN" -q -a $DEFAULT_ALGORITHM -n zone -M 32768:65535 "$zone") +kid=$(keyfile_to_key_id "$ksk") +zid=$(keyfile_to_key_id "$zsk") +[ $kid -ge 0 -a $kid -le 32767 ] || ret=1 +[ $zid -ge 32768 -a $zid -le 65535 ] || ret=1 +rksk=$($REVOKE -R $ksk) +rzsk=$($REVOKE -R $zsk) +krid=$(keyfile_to_key_id "$rksk") +zrid=$(keyfile_to_key_id "$rzsk") +[ $krid -ge 0 -a $krid -le 32767 ] || ret=1 +[ $zrid -ge 32768 -a $zrid -le 65535 ] || ret=1 +n=$((n + 1)) +if [ "$ret" -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + echo_i "exit status: $status" [ $status -eq 0 ] || exit 1 diff --git a/bin/tests/system/kasp.sh b/bin/tests/system/kasp.sh index 6b9c26c4ca..636d3c4da2 100644 --- a/bin/tests/system/kasp.sh +++ b/bin/tests/system/kasp.sh @@ -92,6 +92,7 @@ key_stat() { key_save() { # Save key id. key_set "$1" ID "$KEY_ID" + key_set "$1" RID "$KEY_RID" # Save base filename. key_set "$1" BASEFILE "$BASE_FILE" # Save creation date. @@ -107,6 +108,7 @@ key_save() { # This will update either the KEY1, KEY2, or KEY3 array. key_clear() { key_set "$1" "ID" 'no' + key_set "$1" "RID" 'no' key_set "$1" "IDPAD" 'no' key_set "$1" "EXPECT" 'no' key_set "$1" "ROLE" 'none' @@ -407,6 +409,9 @@ check_key() { [ "$ret" -eq 0 ] || _log_error "${BASE_FILE} files missing" [ "$ret" -eq 0 ] || return 0 + # Retrieve revoked key id + KEY_RID=$($REVOKE -R ${BASE_FILE}) + # Retrieve creation date. grep "; Created:" "$KEY_FILE" >"${ZONE}.${KEY_ID}.${_alg_num}.created" || _log_error "mismatch created comment in $KEY_FILE" KEY_CREATED=$(awk '{print $3}' <"${ZONE}.${KEY_ID}.${_alg_num}.created") diff --git a/bin/tests/system/kasp/ns3/named-fips.conf.in b/bin/tests/system/kasp/ns3/named-fips.conf.in index 3ca1551c29..793cdb5d95 100644 --- a/bin/tests/system/kasp/ns3/named-fips.conf.in +++ b/bin/tests/system/kasp/ns3/named-fips.conf.in @@ -219,6 +219,17 @@ zone "multisigner-model2.kasp" { allow-update { any; }; }; +/* + * A zone that starts with keys that have tags that are + * outside of the desired multi-signer key tag range. + */ +zone "single-to-multisigner.kasp" { + type primary; + file "single-to-multisigner.kasp.db"; + dnssec-policy "multisigner-model2"; + allow-update { any; }; +}; + /* * Different algorithms. */ diff --git a/bin/tests/system/kasp/ns3/policies/kasp-fips.conf.in b/bin/tests/system/kasp/ns3/policies/kasp-fips.conf.in index 7b775f14b8..93ca7293cb 100644 --- a/bin/tests/system/kasp/ns3/policies/kasp-fips.conf.in +++ b/bin/tests/system/kasp/ns3/policies/kasp-fips.conf.in @@ -37,8 +37,8 @@ dnssec-policy "multisigner-model2" { inline-signing no; keys { - ksk key-directory lifetime unlimited algorithm @DEFAULT_ALGORITHM@; - zsk key-directory lifetime unlimited algorithm @DEFAULT_ALGORITHM@; + ksk key-directory lifetime unlimited algorithm @DEFAULT_ALGORITHM@ tag-range 32768 65535; + zsk key-directory lifetime unlimited algorithm @DEFAULT_ALGORITHM@ tag-range 32768 65535; }; }; diff --git a/bin/tests/system/kasp/ns3/setup.sh b/bin/tests/system/kasp/ns3/setup.sh index 69150ada52..40299c78be 100644 --- a/bin/tests/system/kasp/ns3/setup.sh +++ b/bin/tests/system/kasp/ns3/setup.sh @@ -130,8 +130,8 @@ $KEYGEN -G -k rsasha256 -l policies/kasp.conf $zone >keygen.out.$zone.2 2>&1 zone="multisigner-model2.kasp" echo_i "setting up zone: $zone" # Import the ZSK sets of the other providers into their DNSKEY RRset. -ZSK1=$($KEYGEN -K ../ -a $DEFAULT_ALGORITHM -L 3600 $zone 2>keygen.out.$zone.1) -ZSK2=$($KEYGEN -K ../ -a $DEFAULT_ALGORITHM -L 3600 $zone 2>keygen.out.$zone.2) +ZSK1=$($KEYGEN -K ../ -a $DEFAULT_ALGORITHM -L 3600 -M 0:32767 $zone 2>keygen.out.$zone.1) +ZSK2=$($KEYGEN -K ../ -a $DEFAULT_ALGORITHM -L 3600 -M 0:32767 $zone 2>keygen.out.$zone.2) # ZSK1 will be added to the unsigned zonefile. cat "../${ZSK1}.key" | grep -v ";.*" >>"${zone}.db" cat "../${ZSK1}.key" | grep -v ";.*" >"${zone}.zsk1" @@ -184,6 +184,17 @@ cat template.db.in "${CSK}.key" >"$infile" cp $infile $zonefile $SIGNER -PS -z -x -s now-2w -e now-1mi -o $zone -f "${zonefile}.signed" $infile >signer.out.$zone.1 2>&1 +# We are changing an existing single-signed zone to multi-signed +# zone where the key tags do not match the dnssec-policy key tag range +setup single-to-multisigner.kasp +T="now-1d" +KSK=$($KEYGEN -a $DEFAULT_ALGORITHM -M 0:32767 -L 3600 -f KSK $ksktimes $zone 2>keygen.out.$zone.1) +ZSK=$($KEYGEN -a $DEFAULT_ALGORITHM -M 0:32767 -L 3600 $zsktimes $zone 2>keygen.out.$zone.2) +$SETTIME -s -g $O -d $O $T -k $O $T -r $O $T "$KSK" >settime.out.$zone.1 2>&1 +$SETTIME -s -g $O -k $O $T -z $O $T "$ZSK" >settime.out.$zone.2 2>&1 +cat template.db.in "${KSK}.key" "${ZSK}.key" >"$infile" +$SIGNER -PS -z -x -s now-2w -e now-1mi -o $zone -f "${zonefile}" $infile >signer.out.$zone.1 2>&1 + # These signatures are set to expire long in the past, update immediately. setup expired-sigs.autosign T="now-6mo" diff --git a/bin/tests/system/kasp/tests.sh b/bin/tests/system/kasp/tests.sh index 7edfec391c..f2479e6368 100644 --- a/bin/tests/system/kasp/tests.sh +++ b/bin/tests/system/kasp/tests.sh @@ -2154,6 +2154,86 @@ retry_quiet 10 zsks_are_published || ret=1 test "$ret" -eq 0 || echo_i "failed" status=$((status + ret)) +# +# A zone transitioning from single-signed to multi-signed. +# We should have the old omnipresent keys outside of the +# desired key range and the new keys in the desired key range +# KEY1 and KEY2 are the new keys. KEY3 and KEY4 are the old keys. +# +set_zone "single-to-multisigner.kasp" +set_policy "multisigner-model2" "4" "3600" +set_server "ns3" "10.53.0.3" +key_clear "KEY1" +key_clear "KEY2" +key_clear "KEY3" +key_clear "KEY4" + +# Key properties. +set_keyrole "KEY1" "ksk" +set_keylifetime "KEY1" "0" +set_keyalgorithm "KEY1" "$DEFAULT_ALGORITHM_NUMBER" "$DEFAULT_ALGORITHM" "$DEFAULT_BITS" +set_keysigning "KEY1" "yes" +set_zonesigning "KEY1" "no" + +set_keyrole "KEY2" "zsk" +set_keylifetime "KEY2" "0" +set_keyalgorithm "KEY2" "$DEFAULT_ALGORITHM_NUMBER" "$DEFAULT_ALGORITHM" "$DEFAULT_BITS" +set_keysigning "KEY2" "no" +set_zonesigning "KEY2" "no" # waiting for DNSKEY to be omnipresent + +set_keyrole "KEY3" "ksk" +set_keyalgorithm "KEY3" "$DEFAULT_ALGORITHM_NUMBER" "$DEFAULT_ALGORITHM" "$DEFAULT_BITS" +set_keysigning "KEY3" "yes" +set_zonesigning "KEY3" "no" + +set_keyrole "KEY4" "zsk" +set_keyalgorithm "KEY4" "$DEFAULT_ALGORITHM_NUMBER" "$DEFAULT_ALGORITHM" "$DEFAULT_BITS" +set_keysigning "KEY4" "no" +set_zonesigning "KEY4" "yes" + +set_keystate "KEY1" "GOAL" "omnipresent" +set_keystate "KEY1" "STATE_DNSKEY" "rumoured" +set_keystate "KEY1" "STATE_KRRSIG" "rumoured" +set_keystate "KEY1" "STATE_DS" "hidden" + +set_keystate "KEY2" "GOAL" "omnipresent" +set_keystate "KEY2" "STATE_DNSKEY" "rumoured" +set_keystate "KEY2" "STATE_ZRRSIG" "hidden" # waiting for DNSKEY to be omnipresent + +set_keystate "KEY3" "GOAL" "hidden" +set_keystate "KEY3" "STATE_DNSKEY" "omnipresent" +set_keystate "KEY3" "STATE_KRRSIG" "omnipresent" +set_keystate "KEY3" "STATE_DS" "omnipresent" + +set_keystate "KEY4" "GOAL" "hidden" +set_keystate "KEY4" "STATE_DNSKEY" "omnipresent" +set_keystate "KEY4" "STATE_ZRRSIG" "omnipresent" + +check_keys +check_dnssecstatus "$SERVER" "$POLICY" "$ZONE" +check_apex +check_subdomain +dnssec_verify + +# KEY1 tag range 32768 65535 +# KEY2 tag range 32768 65535 +# KEY3 tag range 0 32767 +# KEY4 tag range 0 32767 +n=$((n + 1)) +echo_i "check that the key IDs are in the expected ranges ($n)" +ret=0 +test $(key_get KEY1 ID) -ge 32768 -a $(key_get KEY1 ID) -le 65535 || ret=1 +test $(key_get KEY2 ID) -ge 32768 -a $(key_get KEY2 ID) -le 65535 || ret=1 +test $(key_get KEY3 ID) -ge 0 -a $(key_get KEY3 ID) -le 32767 || ret=1 +test $(key_get KEY4 ID) -ge 0 -a $(key_get KEY4 ID) -le 32767 || ret=1 + +test $(key_get KEY1 RID) -ge 32768 -a $(key_get KEY1 RID) -le 65535 || ret=1 +test $(key_get KEY2 RID) -ge 32768 -a $(key_get KEY2 RID) -le 65535 || ret=1 +test $(key_get KEY3 RID) -ge 0 -a $(key_get KEY3 RID) -le 32767 || ret=1 +test $(key_get KEY4 RID) -ge 0 -a $(key_get KEY4 RID) -le 32767 || ret=1 +test "$ret" -eq 0 || echo_i "failed" +status=$((status + ret)) + # # Testing manual rollover. # diff --git a/doc/arm/reference.rst b/doc/arm/reference.rst index f427f6b02f..75f4fe938b 100644 --- a/doc/arm/reference.rst +++ b/doc/arm/reference.rst @@ -6474,7 +6474,7 @@ The following options can be specified in a :any:`dnssec-policy` statement: keys { ksk key-directory lifetime unlimited algorithm rsasha256 2048; - zsk lifetime 30d algorithm 8; + zsk lifetime 30d algorithm 8 tag-range 0 32767; csk key-store "hsm" lifetime P6MT12H3M15S algorithm ecdsa256; }; @@ -6498,6 +6498,11 @@ The following options can be specified in a :any:`dnssec-policy` statement: When using ``key-directory``, the key is stored in the zone's configured :any:`key-directory`. This is also the default. + When using ``tag-range``, valid key tags for managed keys are + restricted to this range [``tag-min`` ``tag-max``]. The optional + ``tag-range`` is intended to be used in multi-signer scenarios. + The default is unlimited ([0..65535]). + The ``lifetime`` parameter specifies how long a key may be used before rolling over. For convenience, TTL-style time-unit suffixes can be used to specify the key lifetime. It also accepts ISO 8601 diff --git a/doc/misc/options b/doc/misc/options index 0785c4d7bf..dabaad9b68 100644 --- a/doc/misc/options +++ b/doc/misc/options @@ -15,7 +15,7 @@ dnssec-policy { cds-digest-types { ; ... }; dnskey-ttl ; inline-signing ; - keys { ( csk | ksk | zsk ) [ key-directory | key-store ] lifetime algorithm [ ]; ... }; + keys { ( csk | ksk | zsk ) [ key-directory | key-store ] lifetime algorithm [ tag-range ] [ ]; ... }; max-zone-ttl ; nsec3param [ iterations ] [ optout ] [ salt-length ]; offline-ksk ; diff --git a/lib/dns/include/dns/kasp.h b/lib/dns/include/dns/kasp.h index 26af469546..674e733551 100644 --- a/lib/dns/include/dns/kasp.h +++ b/lib/dns/include/dns/kasp.h @@ -58,6 +58,8 @@ struct dns_kasp_key { uint8_t algorithm; int length; uint8_t role; + uint16_t tag_min; + uint16_t tag_max; }; struct dns_kasp_nsec3param { @@ -721,6 +723,26 @@ dns_kasp_key_zsk(dns_kasp_key_t *key); * */ +uint16_t +dns_kasp_key_tagmin(dns_kasp_key_t *key); +/*%< + * Returns the minimum permitted key tag value. + * + * Requires: + * + *\li key != NULL + */ + +uint16_t +dns_kasp_key_tagmax(dns_kasp_key_t *key); +/*%< + * Returns the maximum permitted key tag value. + * + * Requires: + * + *\li key != NULL + */ + bool dns_kasp_key_match(dns_kasp_key_t *key, dns_dnsseckey_t *dkey); /*%< diff --git a/lib/dns/kasp.c b/lib/dns/kasp.c index 50ecba05d6..285ae0bb7f 100644 --- a/lib/dns/kasp.c +++ b/lib/dns/kasp.c @@ -401,7 +401,7 @@ dns_kasp_addkey(dns_kasp_t *kasp, dns_kasp_key_t *key) { isc_result_t dns_kasp_key_create(dns_kasp_t *kasp, dns_kasp_key_t **keyp) { dns_kasp_key_t *key = NULL; - dns_kasp_key_t k = { .length = -1 }; + dns_kasp_key_t k = { .tag_max = 0xffff, .length = -1 }; REQUIRE(DNS_KASP_VALID(kasp)); REQUIRE(keyp != NULL && *keyp == NULL); @@ -507,6 +507,18 @@ dns_kasp_key_zsk(dns_kasp_key_t *key) { return (key->role & DNS_KASP_KEY_ROLE_ZSK); } +uint16_t +dns_kasp_key_tagmin(dns_kasp_key_t *key) { + REQUIRE(key != NULL); + return (key->tag_min); +} + +uint16_t +dns_kasp_key_tagmax(dns_kasp_key_t *key) { + REQUIRE(key != NULL); + return (key->tag_min); +} + bool dns_kasp_key_match(dns_kasp_key_t *key, dns_dnsseckey_t *dkey) { isc_result_t ret; @@ -532,6 +544,16 @@ dns_kasp_key_match(dns_kasp_key_t *key, dns_dnsseckey_t *dkey) { if (ret != ISC_R_SUCCESS || role != dns_kasp_key_zsk(key)) { return (false); } + /* Valid key tag range? */ + uint16_t id = dst_key_id(dkey->key); + uint16_t rid = dst_key_rid(dkey->key); + if (id < key->tag_min || id > key->tag_max) { + return (false); + } + if (rid < key->tag_min || rid > key->tag_max) { + return (false); + } + /* Found a match. */ return (true); } diff --git a/lib/dns/keymgr.c b/lib/dns/keymgr.c index 3eb61185e8..782941c396 100644 --- a/lib/dns/keymgr.c +++ b/lib/dns/keymgr.c @@ -426,11 +426,19 @@ keymgr_key_update_lifetime(dns_dnsseckey_t *key, dns_kasp_t *kasp, } static bool -keymgr_keyid_conflict(dst_key_t *newkey, dns_dnsseckeylist_t *keys) { +keymgr_keyid_conflict(dst_key_t *newkey, uint16_t min, uint16_t max, + dns_dnsseckeylist_t *keys) { uint16_t id = dst_key_id(newkey); uint32_t rid = dst_key_rid(newkey); uint32_t alg = dst_key_alg(newkey); + if (id < min || id > max) { + return (true); + } + if (rid < min || rid > max) { + return (true); + } + for (dns_dnsseckey_t *dkey = ISC_LIST_HEAD(*keys); dkey != NULL; dkey = ISC_LIST_NEXT(dkey, link)) { @@ -484,9 +492,11 @@ keymgr_createkey(dns_kasp_key_t *kkey, const dns_name_t *origin, } /* Key collision? */ - conflict = keymgr_keyid_conflict(newkey, keylist); + conflict = keymgr_keyid_conflict(newkey, kkey->tag_min, + kkey->tag_max, keylist); if (!conflict) { - conflict = keymgr_keyid_conflict(newkey, newkeys); + conflict = keymgr_keyid_conflict( + newkey, kkey->tag_min, kkey->tag_max, newkeys); } if (conflict) { /* Try again. */ diff --git a/lib/isccfg/kaspconf.c b/lib/isccfg/kaspconf.c index b6da87a235..f8637958fc 100644 --- a/lib/isccfg/kaspconf.c +++ b/lib/isccfg/kaspconf.c @@ -117,6 +117,7 @@ cfg_kaspkey_fromconfig(const cfg_obj_t *config, dns_kasp_t *kasp, uint32_t ksk_min_lifetime, uint32_t zsk_min_lifetime) { isc_result_t result; dns_kasp_key_t *key = NULL; + const cfg_obj_t *tagrange = NULL; /* Create a new key reference. */ result = dns_kasp_key_create(kasp, &key); @@ -291,6 +292,38 @@ cfg_kaspkey_fromconfig(const cfg_obj_t *config, dns_kasp_t *kasp, key->length = size; } + + tagrange = cfg_tuple_get(config, "tag-range"); + if (cfg_obj_istuple(tagrange)) { + uint32_t tag_min = 0, tag_max = 0xffff; + obj = cfg_tuple_get(tagrange, "tag-min"); + tag_min = cfg_obj_asuint32(obj); + if (tag_min > 0xffff) { + cfg_obj_log(obj, ISC_LOG_ERROR, + "dnssec-policy: tag-min " + "too big"); + result = ISC_R_RANGE; + goto cleanup; + } + obj = cfg_tuple_get(tagrange, "tag-max"); + tag_max = cfg_obj_asuint32(obj); + if (tag_max > 0xffff) { + cfg_obj_log(obj, ISC_LOG_ERROR, + "dnssec-policy: tag-max " + "too big"); + result = ISC_R_RANGE; + goto cleanup; + } + if (tag_min >= tag_max) { + cfg_obj_log( + obj, ISC_LOG_ERROR, + "dnssec-policy: tag-min >= tag_max"); + result = ISC_R_RANGE; + goto cleanup; + } + key->tag_min = tag_min; + key->tag_max = tag_max; + } } dns_kasp_addkey(kasp, key); diff --git a/lib/isccfg/namedconf.c b/lib/isccfg/namedconf.c index d70e0eb651..83a8a997bb 100644 --- a/lib/isccfg/namedconf.c +++ b/lib/isccfg/namedconf.c @@ -649,12 +649,73 @@ static keyword_type_t lifetime_kw = { "lifetime", static cfg_type_t cfg_type_lifetime = { "lifetime", parse_keyvalue, print_keyvalue, doc_keyvalue, &cfg_rep_duration, &lifetime_kw }; +/* + * + */ +static void +print_tagrange(cfg_printer_t *pctx, const cfg_obj_t *obj) { + REQUIRE(pctx != NULL); + REQUIRE(obj != NULL); + REQUIRE(obj->type->rep == &cfg_rep_tuple); + + if (cfg_obj_istuple(obj)) { + cfg_print_cstr(pctx, "tag-range "); + cfg_print_tuple(pctx, obj); + } +} + +static cfg_tuplefielddef_t tagrange_fields[] = { + { "tag-min", &cfg_type_uint32, 0 }, + { "tag-max", &cfg_type_uint32, 0 }, + { NULL, NULL, 0 } +}; + +static cfg_type_t cfg_type_tagrange = { "tagrange", cfg_parse_tuple, + print_tagrange, cfg_doc_tuple, + &cfg_rep_tuple, tagrange_fields }; + +static keyword_type_t tagrange_kw = { "tag-range", &cfg_type_tagrange }; +static void +doc_optionaltagrange(cfg_printer_t *pctx, const cfg_type_t *type) { + UNUSED(type); + + cfg_print_cstr(pctx, "[ tag-range ]"); +} + +static isc_result_t +parse_optionaltagrange(cfg_parser_t *pctx, const cfg_type_t *type, + cfg_obj_t **ret) { + isc_result_t result; + cfg_obj_t *obj = NULL; + + UNUSED(type); + + CHECK(cfg_peektoken(pctx, 0)); + if (pctx->token.type == isc_tokentype_string && + strcasecmp(TOKEN_STRING(pctx), "tag-range") == 0) + { + CHECK(cfg_gettoken(pctx, CFG_LEXOPT_QSTRING)); + CHECK(cfg_parse_obj(pctx, &cfg_type_tagrange, &obj)); + } else { + CHECK(cfg_parse_void(pctx, NULL, &obj)); + } + + *ret = obj; +cleanup: + return (result); +} + +static cfg_type_t cfg_type_optional_tagrange = { + "optionaltagrange", parse_optionaltagrange, NULL, + doc_optionaltagrange, &cfg_rep_tuple, &tagrange_kw +}; static cfg_tuplefielddef_t kaspkey_fields[] = { { "role", &cfg_type_dnsseckeyrole, 0 }, { "keystorage", &cfg_type_optional_keystore, 0 }, { "lifetime", &cfg_type_lifetime, 0 }, { "algorithm", &cfg_type_algorithm, 0 }, + { "tag-range", &cfg_type_optional_tagrange, 0 }, { "length", &cfg_type_optional_uint32, 0 }, { NULL, NULL, 0 } };