diff --git a/bin/tests/system/checkconf/good-kasp.conf b/bin/tests/system/checkconf/good-kasp.conf index e0a3241368..a4ab5e5557 100644 --- a/bin/tests/system/checkconf/good-kasp.conf +++ b/bin/tests/system/checkconf/good-kasp.conf @@ -40,6 +40,17 @@ dnssec-policy "test" { signatures-validity P2W; signatures-validity-dnskey P14D; zone-propagation-delay PT5M; + zonemd yes; +}; +dnssec-policy "nozm" { + zonemd no; +}; +dnssec-policy "zm512" { + zonemd simple sha512; +}; +dnssec-policy "zm384andzm512" { + zonemd simple sha384; + zonemd simple sha512; }; key-store "hsm" { directory "."; diff --git a/bin/tests/system/kasp/ns3/named-fips.conf.in b/bin/tests/system/kasp/ns3/named-fips.conf.in index 665b37821e..325a90a6f7 100644 --- a/bin/tests/system/kasp/ns3/named-fips.conf.in +++ b/bin/tests/system/kasp/ns3/named-fips.conf.in @@ -237,6 +237,16 @@ zone "max-zone-ttl.kasp" { dnssec-policy "ttl"; }; + +/* + * Zone that uses ZONEMD. + */ +zone "zonemd.kasp" { + type primary; + file "zonemd.kasp.db"; + dnssec-policy "zonemd"; +}; + /* * Zones in different signing states. */ 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 68f932bcf1..a6061b35bf 100644 --- a/bin/tests/system/kasp/ns3/policies/kasp-fips.conf.in +++ b/bin/tests/system/kasp/ns3/policies/kasp-fips.conf.in @@ -119,3 +119,13 @@ dnssec-policy "keystore" { zsk key-store "zsk" lifetime unlimited algorithm @DEFAULT_ALGORITHM@; }; }; + +dnssec-policy "zonemd" { + dnskey-ttl 305; + zonemd yes; + + keys { + ksk key-store "ksk" lifetime unlimited algorithm @DEFAULT_ALGORITHM@; + zsk key-store "zsk" lifetime unlimited algorithm @DEFAULT_ALGORITHM@; + }; +}; diff --git a/bin/tests/system/kasp/ns3/setup.sh b/bin/tests/system/kasp/ns3/setup.sh index 756c0af19c..11aa6d90c6 100644 --- a/bin/tests/system/kasp/ns3/setup.sh +++ b/bin/tests/system/kasp/ns3/setup.sh @@ -50,7 +50,7 @@ for zn in default dnssec-keygen some-keys legacy-keys pregenerated \ rumoured rsasha256 rsasha512 ecdsa256 ecdsa384 \ dynamic dynamic-inline-signing inline-signing \ checkds-ksk checkds-doubleksk checkds-csk inherit unlimited \ - keystore; do + keystore zonemd; do setup "${zn}.kasp" cp template.db.in "$zonefile" done diff --git a/bin/tests/system/kasp/tests_kasp.py b/bin/tests/system/kasp/tests_kasp.py index 4e91565a78..f555cbb3ec 100644 --- a/bin/tests/system/kasp/tests_kasp.py +++ b/bin/tests/system/kasp/tests_kasp.py @@ -1768,3 +1768,10 @@ def test_kasp_manual_mode(ns3): isctest.kasp.check_keys(zone, keys, expected) check_all(ns3, zone, policy, ksks, zsks, manual_mode=True) isctest.kasp.check_dnssec_verify(ns3, zone) + + +def test_kasp_zonemd(ns3): + msg = isctest.query.create("zonemd.kasp", "zonemd") + res = isctest.query.tcp(msg, ns3.ip) + isctest.check.noerror(res) + isctest.check.rr_count_eq(res.answer, 2) diff --git a/doc/arm/reference.rst b/doc/arm/reference.rst index b4cdf63abf..b45ee6c2cf 100644 --- a/doc/arm/reference.rst +++ b/doc/arm/reference.rst @@ -6605,6 +6605,16 @@ max-zone-ttl zone is updated to the time when the new version is served by all of the parent zone's name servers. The default is ``PT1H`` (1 hour). +.. namedconf:statement:: zonemd + :tags: dnssec + :short: Specifies whether a ZONEMD record should be published in an inline-signing zone. + + This option controls whether to publish a ZONEMD record in the + signed version of an :any:`inline-signing` zone. If set to ``no``, + no ZONEMD is published. If set to ``simple sha384`` or ``simple sha512``, + a ZONEMD will be published using the SIMPLE scheme and either the + SHA384 and SHA512 digest type. ``yes`` is a synonym for ``simple sha384``. + Automated KSK Rollovers ^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/misc/options b/doc/misc/options index cd38b058e5..6521261a72 100644 --- a/doc/misc/options +++ b/doc/misc/options @@ -30,6 +30,7 @@ dnssec-policy { signatures-validity ; signatures-validity-dnskey ; zone-propagation-delay ; + zonemd ( simple | ) [ ( sha384 | sha512 ) ]; // may occur multiple times }; // may occur multiple times dyndb { }; // may occur multiple times diff --git a/lib/dns/include/dns/kasp.h b/lib/dns/include/dns/kasp.h index 999e08ddcb..f9e125d470 100644 --- a/lib/dns/include/dns/kasp.h +++ b/lib/dns/include/dns/kasp.h @@ -31,7 +31,9 @@ #include #include +#include #include +#include /* For storing a list of digest types */ struct dns_kasp_digest { @@ -110,6 +112,10 @@ struct dns_kasp { bool inline_signing; bool manual_mode; + /* ZONEMD settings */ + uint8_t zonemd_scheme[DNS_ZONEMD_MAX]; + uint8_t zonemd_digest[DNS_ZONEMD_MAX]; + /* Parent settings */ dns_ttl_t parent_ds_ttl; uint32_t parent_propagation_delay; @@ -900,3 +906,30 @@ dns_kasp_adddigest(dns_kasp_t *kasp, dns_dsdigest_t alg); * *\li 'kasp' is a valid, thawed kasp. */ + +void +dns_kasp_setzonemd(dns_kasp_t *kasp, uint8_t scheme, uint8_t digest); +/*%< + * Set the scheme and digest type for a ZONEMD record. If 'scheme' and + * 'digest' are nonzero, then if no ZONEMD already exists in the zone, + * one will be added after signing. + * + * Requires: + * + *\li 'kasp' is a valid, thawed kasp. + * + *\li 'scheme' and 'digest' are both 0, or else 'scheme' and 'digest' + * are valid ZONEMD scheme/digest values. + */ + +uint8_t * +dns_kasp_zonemd_scheme(dns_kasp_t *kasp); +uint8_t * +dns_kasp_zonemd_digest(dns_kasp_t *kasp); +/*%< + * Get the ZONEMD scheme/digest settings for the zone. + * + * Requires: + * + *\li 'kasp' is a valid, frozen kasp. + */ diff --git a/lib/dns/kasp.c b/lib/dns/kasp.c index 884e3e33ec..c440eacfee 100644 --- a/lib/dns/kasp.c +++ b/lib/dns/kasp.c @@ -681,3 +681,64 @@ dns_kasp_adddigest(dns_kasp_t *kasp, dns_dsdigest_t alg) { ISC_LINK_INIT(digest, link); ISC_LIST_APPEND(kasp->digests, digest, link); } + +void +dns_kasp_setzonemd(dns_kasp_t *kasp, uint8_t scheme, uint8_t digest) { + REQUIRE(kasp != NULL); + REQUIRE(!kasp->frozen); + REQUIRE((scheme == 0 && digest == 0) || + (scheme == DNS_ZONEMD_SCHEME_MAX && + digest == DNS_ZONEMD_DIGEST_MAX) || + (scheme != 0 && digest != 0 && scheme < DNS_ZONEMD_SCHEME_MAX && + digest < DNS_ZONEMD_DIGEST_MAX)); + + /* + * Delete zonemds. + */ + if (scheme == DNS_ZONEMD_SCHEME_MAX) { + kasp->zonemd_scheme[0] = scheme; + kasp->zonemd_digest[0] = digest; + return; + } + + /* + * Maintain zonemds. + */ + if (scheme == 0) { + kasp->zonemd_scheme[0] = scheme; + kasp->zonemd_digest[0] = digest; + return; + } + + /* + * Set to this set. + */ + for (size_t i = 0; i < ARRAY_SIZE(kasp->zonemd_scheme) - 1; i++) { + if (kasp->zonemd_scheme[i] == scheme && + kasp->zonemd_digest[i] == digest) + { + return; + } + if (kasp->zonemd_scheme[i] == 0) { + kasp->zonemd_scheme[i] = scheme; + kasp->zonemd_digest[i] = digest; + return; + } + } +} + +uint8_t * +dns_kasp_zonemd_scheme(dns_kasp_t *kasp) { + REQUIRE(kasp != NULL); + REQUIRE(kasp->frozen); + + return kasp->zonemd_scheme; +} + +uint8_t * +dns_kasp_zonemd_digest(dns_kasp_t *kasp) { + REQUIRE(kasp != NULL); + REQUIRE(kasp->frozen); + + return kasp->zonemd_digest; +} diff --git a/lib/dns/rdata/generic/zonemd_63.h b/lib/dns/rdata/generic/zonemd_63.h index 07adb0c7a0..dd0046da9d 100644 --- a/lib/dns/rdata/generic/zonemd_63.h +++ b/lib/dns/rdata/generic/zonemd_63.h @@ -14,11 +14,25 @@ #pragma once /* Known scheme type(s). */ -#define DNS_ZONEMD_SCHEME_SIMPLE (1) +enum { + DNS_ZONEMD_SCHEME_SIMPLE = 1, + DNS_ZONEMD_SCHEME_MAX = 2, +}; /* Known digest type(s). */ -#define DNS_ZONEMD_DIGEST_SHA384 (1) -#define DNS_ZONEMD_DIGEST_SHA512 (2) +enum { + DNS_ZONEMD_DIGEST_SHA384 = 1, + DNS_ZONEMD_DIGEST_SHA512 = 2, + DNS_ZONEMD_DIGEST_MAX = 3, +}; + +/* + * Array size that can hold all possible combinations of schemes and digests + * with a sentinal (0, 0) entry. + */ +#define DNS_ZONEMD_MAX \ + (((int)DNS_ZONEMD_SCHEME_MAX - 1) * ((int)DNS_ZONEMD_DIGEST_MAX - 1) + \ + 1) /* * \brief per RFC 8976 diff --git a/lib/dns/zone.c b/lib/dns/zone.c index cae04160a4..b687db0418 100644 --- a/lib/dns/zone.c +++ b/lib/dns/zone.c @@ -10242,6 +10242,132 @@ failure: return false; } +static isc_result_t +setup_zonemd(dns_zone_t *zone, dns_db_t *db, dns_dbversion_t *version, + dns_diff_t *diff) { + isc_result_t result; + dns_dbnode_t *node = NULL; + dns_rdata_zonemd_t zonemd; + dns_rdataset_t zmset = DNS_RDATASET_INIT; + unsigned char digest[64] = { 0 }; + unsigned char zmbuf[6 + sizeof(digest)]; + uint8_t *scheme, *digest_type; + dns_ttl_t ttl = 0; + isc_buffer_t b; + + scheme = dns_kasp_zonemd_scheme(zone->kasp); + digest_type = dns_kasp_zonemd_digest(zone->kasp); + + /* + * No zonemd processing specified. + */ + if (scheme[0] == 0) { + return ISC_R_SUCCESS; + } + + CHECK(dns_db_getoriginnode(db, &node)); + + /* Is there already a ZONEMD? */ + result = dns_db_findrdataset(db, node, version, dns_rdatatype_zonemd, 0, + 0, &zmset, NULL); + if (result == ISC_R_SUCCESS) { + ttl = zmset.ttl; + } + + /* + * Delete existing ZONEMD records that don't meet policy. + */ + if (result == ISC_R_SUCCESS && scheme[0] != 0) { + DNS_RDATASET_FOREACH(&zmset) { + dns_rdata_t zr = DNS_RDATA_INIT; + bool found = false; + + /* + * DNS_ZONEMD_SCHEME_MAX => delete everything. + */ + if (scheme[0] != DNS_ZONEMD_SCHEME_MAX) { + dns_rdataset_current(&zmset, &zr); + (void)dns_rdata_tostruct(&zr, &zonemd, NULL); + + for (size_t i = 0; scheme[i] != 0; i++) { + if (zonemd.scheme == scheme[i] && + zonemd.digest_type == digest[i]) + { + found = true; + } + } + } + + /* + * Is there a scheme/digest_type mismatch, or ZONEMD is + * being removed? + */ + if (!found) { + CHECK(update_one_rr(db, version, diff, + DNS_DIFFOP_DEL, + &zone->origin, ttl, &zr)); + } + } + } else if (result == ISC_R_NOTFOUND) { + result = ISC_R_SUCCESS; + } + + if (result != ISC_R_SUCCESS || scheme[0] == DNS_ZONEMD_SCHEME_MAX) { + goto failure; + } + + if (!dns_rdataset_isassociated(&zmset)) { + /* Get the SOA TTL */ + dns_rdataset_t soaset = DNS_RDATASET_INIT; + CHECK(dns_db_findrdataset(db, node, version, dns_rdatatype_soa, + 0, 0, &soaset, NULL)); + ttl = soaset.ttl; + dns_rdataset_disassociate(&soaset); + } + + /* ZONEMD was either not present or has been deleted: add a dummy */ + zonemd.common.rdclass = zone->rdclass; + zonemd.common.rdtype = dns_rdatatype_zonemd; + zonemd.mctx = NULL; + zonemd.serial = 0; + zonemd.digest = digest; + + for (size_t i = 0; scheme[i] != 0; i++) { + dns_rdata_t rdata = DNS_RDATA_INIT; + + zonemd.scheme = scheme[i]; + zonemd.digest_type = digest_type[i]; + + switch (zonemd.digest_type) { + case DNS_ZONEMD_DIGEST_SHA384: + zonemd.length = ISC_SHA384_DIGESTLENGTH; + break; + case DNS_ZONEMD_DIGEST_SHA512: + zonemd.length = ISC_SHA512_DIGESTLENGTH; + break; + default: + UNREACHABLE(); + } + + isc_buffer_init(&b, zmbuf, sizeof(zmbuf)); + CHECK(dns_rdata_fromstruct(&rdata, zone->rdclass, + dns_rdatatype_zonemd, &zonemd, &b)); + + CHECK(update_one_rr(db, version, diff, DNS_DIFFOP_ADD, + &zone->origin, ttl, &rdata)); + } + +failure: + if (dns_rdataset_isassociated(&zmset)) { + dns_rdataset_disassociate(&zmset); + } + if (node != NULL) { + dns_db_detachnode(&node); + } + + return result; +} + /* * Incrementally sign the zone using the keys requested. * Builds the NSEC chain if required. @@ -10324,6 +10450,23 @@ zone_sign(dns_zone_t *zone) { goto cleanup; } + if (zone->kasp != NULL) { + result = setup_zonemd(zone, db, version, &_sig_diff); + if (result != ISC_R_SUCCESS) { + dnssec_log(zone, ISC_LOG_ERROR, + "zone_sign:setup_zonemd -> %s", + isc_result_totext(result)); + goto cleanup; + } + } + result = dns_update_zonemd(db, version, &_sig_diff); + if (result != ISC_R_SUCCESS) { + dnssec_log(zone, ISC_LOG_ERROR, + "zone_sign:dns_update_zonemd -> %s", + isc_result_totext(result)); + goto cleanup; + } + kasp = zone->kasp; calculate_rrsig_validity(zone, now, &inception, &soaexpire, NULL, @@ -17321,7 +17464,8 @@ sync_secure_journal(dns_zone_t *zone, dns_zone_t *raw, dns_journal_t *journal, if (rdata->type == dns_rdatatype_nsec || rdata->type == dns_rdatatype_rrsig || rdata->type == dns_rdatatype_nsec3 || - rdata->type == dns_rdatatype_nsec3param) + rdata->type == dns_rdatatype_nsec3param || + rdata->type == dns_rdatatype_zonemd) { continue; } @@ -17510,7 +17654,8 @@ sync_secure_db(dns_zone_t *seczone, dns_zone_t *raw, dns_db_t *secdb, if (tuple->rdata.type == dns_rdatatype_nsec || tuple->rdata.type == dns_rdatatype_rrsig || tuple->rdata.type == dns_rdatatype_nsec3 || - tuple->rdata.type == dns_rdatatype_nsec3param) + tuple->rdata.type == dns_rdatatype_nsec3param || + tuple->rdata.type == dns_rdatatype_zonemd) { ISC_LIST_UNLINK(diff->tuples, tuple, link); dns_difftuple_free(&tuple); @@ -17732,6 +17877,14 @@ receive_secure_serial(void *arg) { dns_db_currentversion(zone->rss_db, &zone->rss_oldver); CHECK(dns_db_newversion(zone->rss_db, &zone->rss_newver)); + /* + * Set up the ZONEMD as specified in the policy. + */ + CHECK(setup_zonemd(zone, zone->rss_db, zone->rss_newver, + &zone->rss_diff)); + CHECK(dns_update_zonemd(zone->rss_db, zone->rss_newver, + &zone->rss_diff)); + /* * Try to apply diffs from the raw zone's journal to the secure * zone. If that fails, we recover by syncing up the databases @@ -18174,7 +18327,8 @@ copy_non_dnssec_records(dns_db_t *db, dns_dbversion_t *version, dns_db_t *rawdb, if (rdataset.type == dns_rdatatype_nsec || rdataset.type == dns_rdatatype_rrsig || rdataset.type == dns_rdatatype_nsec3 || - rdataset.type == dns_rdatatype_nsec3param) + rdataset.type == dns_rdatatype_nsec3param || + rdataset.type == dns_rdatatype_zonemd) { dns_rdataset_disassociate(&rdataset); continue; @@ -18274,11 +18428,8 @@ receive_secure_db(void *arg) { } DNS_DBITERATOR_FOREACH(dbiterator) { - result = copy_non_dnssec_records(db, version, rawdb, dbiterator, - oldserialp); - if (result != ISC_R_SUCCESS) { - goto failure; - } + CHECK(copy_non_dnssec_records(db, version, rawdb, dbiterator, + oldserialp)); } dns_dbiterator_destroy(&dbiterator); diff --git a/lib/isccfg/kaspconf.c b/lib/isccfg/kaspconf.c index d83e0af46b..bb49ebb6e2 100644 --- a/lib/isccfg/kaspconf.c +++ b/lib/isccfg/kaspconf.c @@ -30,6 +30,7 @@ #include #include #include +#include #include @@ -464,6 +465,7 @@ cfg_kasp_fromconfig(const cfg_obj_t *config, dns_kasp_t *default_kasp, const cfg_obj_t *inlinesigning = NULL; const cfg_obj_t *cds = NULL; const cfg_obj_t *obj = NULL; + const cfg_obj_t *zonemd = NULL; const char *kaspname = NULL; dns_kasp_t *kasp = NULL; size_t i = 0; @@ -600,6 +602,58 @@ cfg_kasp_fromconfig(const cfg_obj_t *config, dns_kasp_t *default_kasp, DNS_KASP_PARENT_PROPDELAY); dns_kasp_setparentpropagationdelay(kasp, parentpropdelay); + (void)confget(maps, "zonemd", &zonemd); + if (zonemd != NULL) { + CFG_LIST_FOREACH(zonemd, element) { + obj = cfg_listelt_value(element); + const cfg_obj_t *sobj = cfg_tuple_get(obj, "scheme"); + if (cfg_obj_isboolean(sobj)) { + if (!cfg_obj_asboolean(sobj)) { + dns_kasp_setzonemd( + kasp, DNS_ZONEMD_SCHEME_MAX, + DNS_ZONEMD_DIGEST_MAX); + } else { + dns_kasp_setzonemd( + kasp, DNS_ZONEMD_SCHEME_SIMPLE, + DNS_ZONEMD_DIGEST_SHA384); + } + } else if (strcasecmp(cfg_obj_asstring(sobj), + "simple") != 0) + { + cfg_obj_log( + config, ISC_LOG_ERROR, + "dnssec-policy: policy '%s' unknown " + "ZONEMD scheme %s", + kaspname, cfg_obj_asstring(sobj)); + result = ISC_R_FAILURE; + } else { + const cfg_obj_t *dobj = cfg_tuple_get(obj, + "digest"); + if (strcasecmp(cfg_obj_asstring(dobj), + "sha384")) + { + dns_kasp_setzonemd( + kasp, DNS_ZONEMD_SCHEME_SIMPLE, + DNS_ZONEMD_DIGEST_SHA384); + } else if (strcasecmp(cfg_obj_asstring(dobj), + "sha512")) + { + dns_kasp_setzonemd( + kasp, DNS_ZONEMD_SCHEME_SIMPLE, + DNS_ZONEMD_DIGEST_SHA512); + } else { + cfg_obj_log(config, ISC_LOG_ERROR, + "dnssec-policy: policy " + "'%s' unknown " + "ZONEMD digest %s", + kaspname, + cfg_obj_asstring(dobj)); + result = ISC_R_FAILURE; + } + } + } + } + /* Configuration: Keys */ obj = NULL; (void)confget(maps, "offline-ksk", &obj); diff --git a/lib/isccfg/namedconf.c b/lib/isccfg/namedconf.c index 5bc2c31bc3..5ebeb5fdf1 100644 --- a/lib/isccfg/namedconf.c +++ b/lib/isccfg/namedconf.c @@ -2205,6 +2205,38 @@ static cfg_type_t cfg_type_checkdstype = { doc_checkds_type, &cfg_rep_string, checkds_enums, }; +/*% + * Zonemd type. + */ +static const char *zonemd_schemes[] = { "simple", NULL }; +static const char *zonemd_digests[] = { "sha384", "sha512", NULL }; + +static isc_result_t +parse_zscheme(cfg_parser_t *pctx, const cfg_type_t *type, cfg_obj_t **ret) { + return cfg_parse_enum_or_other(pctx, type, &cfg_type_boolean, ret); +} +static void +doc_zscheme(cfg_printer_t *pctx, const cfg_type_t *type) { + cfg_doc_enum_or_other(pctx, type, &cfg_type_boolean); +} + +static cfg_type_t cfg_type_zscheme = { "zscheme", parse_zscheme, + cfg_print_ustring, doc_zscheme, + &cfg_rep_string, zonemd_schemes }; +static cfg_type_t cfg_type_zdigest = { "zdigest", parse_optional_enum, + cfg_print_ustring, doc_optional_enum, + &cfg_rep_string, zonemd_digests }; + +static cfg_tuplefielddef_t zonemd_fields[] = { + { "scheme", &cfg_type_zscheme, 0 }, + { "digest", &cfg_type_zdigest, 0 }, + { NULL, NULL, 0 } +}; + +static cfg_type_t cfg_type_zonemd = { "zonemd", cfg_parse_tuple, + cfg_print_tuple, cfg_doc_tuple, + &cfg_rep_tuple, zonemd_fields }; + /*% * Clauses that can be found in a 'dnssec-policy' statement. */ @@ -2229,6 +2261,7 @@ static cfg_clausedef_t dnssecpolicy_clauses[] = { { "signatures-validity", &cfg_type_duration, 0 }, { "signatures-validity-dnskey", &cfg_type_duration, 0 }, { "zone-propagation-delay", &cfg_type_duration, 0 }, + { "zonemd", &cfg_type_zonemd, CFG_CLAUSEFLAG_MULTI }, { NULL, NULL, 0 } };