# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.
+from pathlib import Path
from re import compile as Re
import os
import re
-
+import time
+
+from cryptography.hazmat.primitives.asymmetric import rsa
+from dns.dnssectypes import NSEC3Hash
+from dns.rdtypes.dnskeybase import Flag
+
+import dns.dnssec
+import dns.name
+import dns.rdata
+import dns.rdataclass
+import dns.rdatatype
+import dns.zone
import pytest
+from isctest.template import NO_NS, zones
+from isctest.zone import Zone
+
import isctest
pytestmark = pytest.mark.extra_artifacts(
"zones/*.out*",
"zones/*.tmp",
"zones/updated*",
+ "zones/bad-nsec3param-hash*",
+ "zones/nsec-bad-nsec3param-hash*",
+ "zones/good-bad-nsec3param-hash*",
+ "zones/nsec+non-zero-nsec3param-flags*",
+ "zones/no-nsec+non-zero-nsec3param-flags*",
+ "zones/nseconly-nsec3param*",
]
)
VERIFY = os.environ.get("VERIFY")
+def bootstrap():
+
+ def generate_keys():
+ algorithm = dns.dnssec.Algorithm.RSASHA256
+ ksk_private_key = rsa.generate_private_key(public_exponent=65537, key_size=1024)
+ try:
+ ksk_dnskey = dns.dnssec.make_dnskey(
+ public_key=ksk_private_key.public_key(),
+ algorithm=algorithm,
+ flags=Flag.ZONE | Flag.SEP,
+ )
+ except ImportError as exc:
+ # if the cryptography package is too old, the make_dnskey() function
+ # will raise ImportError at runtime
+ pytest.skip(f"{exc}")
+ return ksk_private_key, ksk_dnskey
+
+ def gen_and_sign(name, nsec3=False):
+ zone = Zone(name, NO_NS, signed=True)
+ ksk_private_key, ksk_dnskey = generate_keys()
+ keys = [(ksk_private_key, ksk_dnskey)]
+ zone.render()
+
+ # read the rendered zone
+ unsigned_path = str(Path(zone.ns.name) / zone.filepath_unsigned)
+ signed_path = str(Path(zone.ns.name) / zone.filepath_signed)
+ zoneobj = dns.zone.from_file(unsigned_path, origin=f"{name}.")
+ lifetime = 30 * 86400
+
+ # sign the zone
+ with zoneobj.writer() as txn:
+ dns.dnssec.sign_zone(
+ zone=zoneobj,
+ txn=txn,
+ keys=keys,
+ lifetime=lifetime,
+ add_dnskey=True,
+ deterministic=False, # for OpenSSL<3.2.0 compat
+ )
+
+ # This generates an NSEC3 record for the apex, so that we can
+ # verify a zone with both a valid and invalid NSEC3PARAM record.
+ if nsec3:
+ origin = dns.name.from_text(f"{name}.")
+ hashname = dns.dnssec.nsec3_hash(
+ origin, salt=b"", iterations=0, algorithm=NSEC3Hash.SHA1
+ )
+ nsec3rdata = dns.rdata.from_text(
+ rdclass=dns.rdataclass.IN,
+ rdtype=dns.rdatatype.NSEC3,
+ tok=f"1 0 0 - {hashname} SOA NS A NSEC3PARAM DNSKEY RRSIG",
+ )
+ nsec3_owner = dns.name.from_text(f"{hashname}.{name}.")
+ node = zoneobj.find_node(nsec3_owner, create=True)
+ nsec3_rrset = node.find_rdataset(
+ rdclass=dns.rdataclass.IN, rdtype=dns.rdatatype.NSEC3, create=True
+ )
+ nsec3_rrset.ttl = 300
+ nsec3_rrset.add(nsec3rdata)
+
+ inception = int(time.time()) - 30
+ nsec3_sigdata = dns.dnssec.sign(
+ rrset=(nsec3_owner, nsec3_rrset),
+ private_key=ksk_private_key,
+ signer=origin,
+ dnskey=ksk_dnskey,
+ inception=inception,
+ lifetime=lifetime,
+ )
+ nsec3_rrsig = node.find_rdataset(
+ rdclass=dns.rdataclass.IN,
+ rdtype=dns.rdatatype.RRSIG,
+ covers=dns.rdatatype.NSEC3,
+ create=True,
+ )
+ nsec3_rrsig.ttl = 300
+ nsec3_rrsig.add(nsec3_sigdata)
+
+ zoneobj.to_file(signed_path)
+
+ return zone
+
+ return {
+ "zones": zones(
+ [
+ gen_and_sign("bad-nsec3param-hash"),
+ gen_and_sign("nsec-bad-nsec3param-hash"),
+ gen_and_sign("good-bad-nsec3param-hash", True),
+ gen_and_sign("nsec+non-zero-nsec3param-flags"),
+ gen_and_sign("no-nsec+non-zero-nsec3param-flags"),
+ gen_and_sign("nseconly-nsec3param", True),
+ ]
+ ),
+ }
+
+
@pytest.mark.parametrize(
"zone",
[
]
)
assert "Loading zone 'updated' from file 'zones/updated.other'" in cmd.out
+
+
+# checking that unknown hash is detected
+def test_verify_rejects_bad_nsec3param_hash():
+ cmd = isctest.run.cmd(
+ [
+ VERIFY,
+ "-z",
+ "-o",
+ "bad-nsec3param-hash",
+ "zones/bad-nsec3param-hash.db.signed",
+ ],
+ raise_on_exception=False,
+ )
+ assert cmd.rc != 0
+ assert "No usable NSEC/NSEC3 chain for testing" in cmd.err
+
+
+def test_verify_ignores_bad_nsec3param_hash_with_nsec():
+ isctest.run.cmd(
+ [
+ VERIFY,
+ "-z",
+ "-o",
+ "nsec-bad-nsec3param-hash",
+ "zones/nsec-bad-nsec3param-hash.db.signed",
+ ]
+ )
+
+
+def test_verify_ignores_bad_nsec3param_hash_with_good_nsec3param():
+ isctest.run.cmd(
+ [
+ VERIFY,
+ "-z",
+ "-o",
+ "good-bad-nsec3param-hash",
+ "zones/good-bad-nsec3param-hash.db.signed",
+ ]
+ )
+
+
+def test_verify_rejects_no_nsec_and_all_nsec3param_with_non_zero_flags():
+ cmd = isctest.run.cmd(
+ [
+ VERIFY,
+ "-z",
+ "-o",
+ "no-nsec+non-zero-nsec3param-flags",
+ "zones/no-nsec+non-zero-nsec3param-flags.db.signed",
+ ],
+ raise_on_exception=False,
+ )
+ assert cmd.rc != 0
+ assert "No usable NSEC/NSEC3 chain for testing" in cmd.err
+
+
+def test_verify_ignores_nsec3param_non_zero_flags():
+ isctest.run.cmd(
+ [
+ VERIFY,
+ "-z",
+ "-o",
+ "nsec+non-zero-nsec3param-flags",
+ "zones/nsec+non-zero-nsec3param-flags.db.signed",
+ ],
+ )
+
+
+def test_verify_rejects_nsec3param_with_nsec_only_key():
+ cmd = isctest.run.cmd(
+ [
+ VERIFY,
+ "-z",
+ "-o",
+ "nseconly-nsec3param",
+ "zones/nseconly-nsec3param.db.signed",
+ ],
+ raise_on_exception=False,
+ )
+ assert cmd.rc != 0
+ isctest.log.debug(cmd.err)
+ assert "cannot use NSEC3 with key algorithm" in cmd.out
dns_keytable_t *secroots;
bool goodksk;
bool goodzsk;
+ bool nseconly;
dns_rdataset_t keyset;
dns_rdataset_t keysigs;
dns_rdataset_t soaset;
static isc_result_t
isoptout(const vctx_t *vctx, const dns_rdata_nsec3param_t *nsec3param,
bool *optout) {
- dns_rdataset_t rdataset;
+ dns_rdataset_t rdataset = DNS_RDATASET_INIT;
dns_rdata_t rdata = DNS_RDATA_INIT;
dns_rdata_nsec3_t nsec3;
dns_fixedname_t fixed;
- dns_name_t *hashname;
+ dns_name_t *hashname = dns_fixedname_initname(&fixed);
isc_result_t result;
dns_dbnode_t *node = NULL;
unsigned char rawhash[NSEC3_MAX_HASH_LENGTH];
size_t rhsize = sizeof(rawhash);
- dns_fixedname_init(&fixed);
result = dns_nsec3_hashname(
&fixed, rawhash, &rhsize, vctx->origin, vctx->origin,
nsec3param->hash, nsec3param->iterations, nsec3param->salt.base,
return result;
}
- dns_rdataset_init(&rdataset);
- hashname = dns_fixedname_name(&fixed);
result = dns_db_findnsec3node(vctx->db, hashname, false, &node);
if (result == ISC_R_SUCCESS) {
result = dns_db_findrdataset(vctx->db, node, vctx->ver,
dns_rdatatype_nsec3, 0, 0,
&rdataset, NULL);
}
+ if (result == ISC_R_SUCCESS) {
+ result = dns_rdataset_first(&rdataset);
+ }
if (result != ISC_R_SUCCESS) {
*optout = false;
result = ISC_R_SUCCESS;
goto done;
}
- result = dns_rdataset_first(&rdataset);
- if (result != ISC_R_SUCCESS) {
- zoneverify_log_error(vctx, "dns_rdataset_first(): %s",
- isc_result_totext(result));
- goto done;
- }
-
dns_rdataset_current(&rdataset, &rdata);
result = dns_rdata_tostruct(&rdata, &nsec3, NULL);
static isc_result_t
verifynsec3(const vctx_t *vctx, const dns_name_t *name,
- const dns_rdata_t *rdata, bool delegation, bool empty,
- const unsigned char types[8192], unsigned int maxtype,
+ const dns_rdata_nsec3param_t *nsec3param, bool delegation,
+ bool empty, const unsigned char types[8192], unsigned int maxtype,
isc_result_t *vresult) {
char namebuf[DNS_NAME_FORMATSIZE];
char hashbuf[DNS_NAME_FORMATSIZE];
- dns_rdataset_t rdataset;
- dns_rdata_nsec3param_t nsec3param;
+ dns_rdataset_t rdataset = DNS_RDATASET_INIT;
dns_fixedname_t fixed;
- dns_name_t *hashname;
- isc_result_t result, tvresult = ISC_R_UNSET;
+ dns_name_t *hashname = dns_fixedname_initname(&fixed);
+ isc_result_t result;
dns_dbnode_t *node = NULL;
unsigned char rawhash[NSEC3_MAX_HASH_LENGTH];
size_t rhsize = sizeof(rawhash);
bool optout = false;
- result = dns_rdata_tostruct(rdata, &nsec3param, NULL);
- RUNTIME_CHECK(result == ISC_R_SUCCESS);
-
- if (nsec3param.flags != 0) {
- return ISC_R_SUCCESS;
- }
-
- if (!dns_nsec3_supportedhash(nsec3param.hash)) {
- return ISC_R_SUCCESS;
- }
+ INSIST(nsec3param->flags == 0);
- if (nsec3param.iterations > DNS_NSEC3_MAXITERATIONS) {
- result = DNS_R_NSEC3ITERRANGE;
- zoneverify_log_error(vctx, "verifynsec3: %s",
- isc_result_totext(result));
+ result = isoptout(vctx, nsec3param, &optout);
+ if (result != ISC_R_SUCCESS) {
+ *vresult = result;
return result;
}
- RETERR(isoptout(vctx, &nsec3param, &optout));
-
- dns_fixedname_init(&fixed);
- result = dns_nsec3_hashname(&fixed, rawhash, &rhsize, name,
- vctx->origin, nsec3param.hash,
- nsec3param.iterations, nsec3param.salt.base,
- nsec3param.salt.length);
+ result = dns_nsec3_hashname(
+ &fixed, rawhash, &rhsize, name, vctx->origin, nsec3param->hash,
+ nsec3param->iterations, nsec3param->salt.base,
+ nsec3param->salt.length);
if (result != ISC_R_SUCCESS) {
zoneverify_log_error(vctx, "dns_nsec3_hashname(): %s",
isc_result_totext(result));
+ *vresult = result;
return result;
}
* from dnssec-signzone so the secure status of the zone may not
* be up to date.
*/
- dns_rdataset_init(&rdataset);
- hashname = dns_fixedname_name(&fixed);
result = dns_db_findnsec3node(vctx->db, hashname, false, &node);
if (result == ISC_R_SUCCESS) {
result = dns_db_findrdataset(vctx->db, node, vctx->ver,
{
result = ISC_R_SUCCESS;
} else if (result == ISC_R_SUCCESS) {
- result = match_nsec3(vctx, name, &nsec3param, &rdataset, types,
+ isc_result_t tvresult = ISC_R_UNSET;
+ result = match_nsec3(vctx, name, nsec3param, &rdataset, types,
maxtype, rawhash, rhsize, &tvresult);
if (result != ISC_R_SUCCESS) {
+ *vresult = tvresult;
goto done;
}
result = tvresult;
const unsigned char types[8192], unsigned int maxtype,
isc_result_t *vresult) {
DNS_RDATASET_FOREACH(nsec3paramset) {
+ isc_result_t result;
dns_rdata_t rdata = DNS_RDATA_INIT;
+ dns_rdata_nsec3param_t nsec3param;
dns_rdataset_current(nsec3paramset, &rdata);
- RETERR(verifynsec3(vctx, name, &rdata, delegation, empty, types,
- maxtype, vresult));
+
+ result = dns_rdata_tostruct(&rdata, &nsec3param, NULL);
+ RUNTIME_CHECK(result == ISC_R_SUCCESS);
+
+ /* Skip unusable NSEC3PARAM records. */
+ if (nsec3param.flags != 0) {
+ continue;
+ }
+
+ /*
+ * If an NSEC-only algorithm is in the DNSKEY set,
+ * any NSEC3PARAM with flags == 0 is an error.
+ */
+ if (vctx->nseconly) {
+ *vresult = DNS_R_NSEC3BADALG;
+ break;
+ }
+
+ if (nsec3param.iterations > DNS_NSEC3_MAXITERATIONS ||
+ !dns_nsec3_supportedhash(nsec3param.hash))
+ {
+ continue;
+ }
+
+ RETERR(verifynsec3(vctx, name, &nsec3param, delegation, empty,
+ types, maxtype, vresult));
if (*vresult != ISC_R_SUCCESS) {
break;
}
check_apex_rrsets(vctx_t *vctx) {
dns_dbnode_t *node = NULL;
isc_result_t result;
+ bool nsec3param_ok = false;
- result = dns_db_findnode(vctx->db, vctx->origin, false, &node);
- if (result != ISC_R_SUCCESS) {
- zoneverify_log_error(vctx,
- "failed to find the zone's origin: %s",
- isc_result_totext(result));
- return result;
+ result = dns_db_getoriginnode(vctx->db, &node);
+ if (result == ISC_R_SUCCESS) {
+ result = dns_db_findrdataset(vctx->db, node, vctx->ver,
+ dns_rdatatype_dnskey, 0, 0,
+ &vctx->keyset, &vctx->keysigs);
}
-
- result = dns_db_findrdataset(vctx->db, node, vctx->ver,
- dns_rdatatype_dnskey, 0, 0, &vctx->keyset,
- &vctx->keysigs);
if (result != ISC_R_SUCCESS) {
zoneverify_log_error(vctx, "Zone contains no DNSSEC keys");
- goto done;
+ CLEANUP(result);
}
result = dns_db_findrdataset(vctx->db, node, vctx->ver,
&vctx->soasigs);
if (result != ISC_R_SUCCESS) {
zoneverify_log_error(vctx, "Zone contains no SOA record");
- goto done;
+ CLEANUP(result)
}
result = dns_db_findrdataset(vctx->db, node, vctx->ver,
&vctx->nsecsigs);
if (result != ISC_R_SUCCESS && result != ISC_R_NOTFOUND) {
zoneverify_log_error(vctx, "NSEC lookup failed");
- goto done;
+ CLEANUP(result);
}
result = dns_db_findrdataset(
&vctx->nsec3paramset, &vctx->nsec3paramsigs);
if (result != ISC_R_SUCCESS && result != ISC_R_NOTFOUND) {
zoneverify_log_error(vctx, "NSEC3PARAM lookup failed");
- goto done;
+ CLEANUP(result);
}
if (!dns_rdataset_isassociated(&vctx->keysigs)) {
zoneverify_log_error(vctx, "DNSKEY is not signed "
"(keys offline or inactive?)");
- result = ISC_R_FAILURE;
- goto done;
+ CLEANUP(ISC_R_FAILURE);
}
if (!dns_rdataset_isassociated(&vctx->soasigs)) {
zoneverify_log_error(vctx, "SOA is not signed "
"(keys offline or inactive?)");
- result = ISC_R_FAILURE;
- goto done;
+ CLEANUP(ISC_R_FAILURE);
}
if (dns_rdataset_isassociated(&vctx->nsecset) &&
{
zoneverify_log_error(vctx, "NSEC is not signed "
"(keys offline or inactive?)");
- result = ISC_R_FAILURE;
- goto done;
+ CLEANUP(ISC_R_FAILURE);
}
if (dns_rdataset_isassociated(&vctx->nsec3paramset) &&
{
zoneverify_log_error(vctx, "NSEC3PARAM is not signed "
"(keys offline or inactive?)");
- result = ISC_R_FAILURE;
- goto done;
+ CLEANUP(ISC_R_FAILURE);
}
- if (!dns_rdataset_isassociated(&vctx->nsecset) &&
- !dns_rdataset_isassociated(&vctx->nsec3paramset))
- {
- zoneverify_log_error(vctx, "No valid NSEC/NSEC3 chain for "
+ /*
+ * Do we have a NSEC3PARAM record that indicates a complete
+ * chain? A forged NSEC3PARAM set will be detected later.
+ */
+ if (dns_rdataset_isassociated(&vctx->nsec3paramset)) {
+ DNS_RDATASET_FOREACH(&vctx->nsec3paramset) {
+ dns_rdata_t rdata = DNS_RDATA_INIT;
+ dns_rdata_nsec3param_t nsec3param;
+
+ dns_rdataset_current(&vctx->nsec3paramset, &rdata);
+ result = dns_rdata_tostruct(&rdata, &nsec3param, NULL);
+ RUNTIME_CHECK(result == ISC_R_SUCCESS);
+ if (nsec3param.flags != 0 ||
+ nsec3param.iterations > DNS_NSEC3_MAXITERATIONS ||
+ !dns_nsec3_supportedhash(nsec3param.hash))
+ {
+ continue;
+ }
+ nsec3param_ok = true;
+ break;
+ }
+ }
+
+ if (!dns_rdataset_isassociated(&vctx->nsecset) && !nsec3param_ok) {
+ zoneverify_log_error(vctx, "No usable NSEC/NSEC3 chain for "
"testing");
- result = ISC_R_FAILURE;
- goto done;
+ CLEANUP(ISC_R_FAILURE);
}
result = ISC_R_SUCCESS;
-done:
+cleanup:
dns_db_detachnode(&node);
return result;
dns_db_detachnode(&node);
goto done;
}
- if (*vresult == ISC_R_UNSET) {
- *vresult = ISC_R_SUCCESS;
- }
- if (*vresult == ISC_R_SUCCESS) {
+ if (*vresult == ISC_R_UNSET || *vresult == ISC_R_SUCCESS) {
*vresult = tvresult;
}
if (prevname != NULL) {
goto done;
}
+ /* Record whether NSEC3 is supported by the DNSKEY RRset */
+ result = dns_nsec_nseconly(vctx.db, vctx.ver, NULL, &vctx.nseconly);
+ RUNTIME_CHECK(result == ISC_R_SUCCESS);
+
determine_active_algorithms(&vctx, ignore_kskflag, keyset_kskonly,
report);
}
result = verify_nsec3_chains(&vctx, mctx);
- if (vresult == ISC_R_UNSET) {
- vresult = ISC_R_SUCCESS;
- }
- if (result != ISC_R_SUCCESS && vresult == ISC_R_SUCCESS) {
+ if (result != ISC_R_SUCCESS &&
+ (vresult == ISC_R_SUCCESS || vresult == ISC_R_UNSET))
+ {
vresult = result;
}