Skip to content

Commit ef88fc4

Browse files
committed
Merge branch 'release-6.0.6' into 6.0
2 parents ac266da + 4451725 commit ef88fc4

17 files changed

+207
-29
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Jiří Helebrant <[email protected]>
3636
Jonathan Coetzee <[email protected]>
3737
Josh Soref <[email protected]>
3838
Karel Slaný <[email protected]>
39+
Kirill A. Korinsky <[email protected]>
3940
Konstantin Amelichev <[email protected]>
4041
Ladislav Lhotka <[email protected]>
4142
Leo Vandewoestijne <[email protected]>

NEWS

+38-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,31 @@
1-
Knot Resolver 6.0.6 (2024-0m-dd)
1+
Knot Resolver 6.0.6 (2024-02-13)
22
================================
33

4+
Security
5+
--------
6+
- CVE-2023-50868: NSEC3 closest encloser proof can exhaust CPU
7+
* validator: lower the NSEC3 iteration limit (150 -> 50)
8+
* validator: similarly also limit excessive NSEC3 salt length
9+
* cache: limit the amount of work on SHA1 in NSEC3 aggressive cache
10+
* validator: limit the amount of work on SHA1 in NSEC3 proofs
11+
* validator: refuse to validate answers with more than 8 NSEC3 records
12+
13+
- CVE-2023-50387 "KeyTrap": DNSSEC verification complexity
14+
could be exploited to exhaust CPU resources and stall DNS resolvers.
15+
Solution boils down mainly to limiting crypto-validations per packet.
16+
17+
We would like to thank Elias Heftrig, Haya Schulmann, Niklas Vogel and Michael Waidner
18+
from the German National Research Center for Applied Cybersecurity ATHENE
19+
for bringing this vulnerability to our attention.
20+
421
Improvements
522
------------
23+
- update addresses of B.root-servers.net (!1478)
624
- tweak the default run_dir on non-Linux (!1481)
725

826
Bugfixes
927
--------
28+
- fix potential SERVFAIL deadlocks if net.ipv6 = false (#880)
1029
- fix validation of RRsets around 64 KiB size; needs libknot >= 3.4 (!1497)
1130

1231

@@ -27,9 +46,26 @@ https://knot.pages.nic.cz/knot-resolver/upgrading-to-6.html
2746
~~~~~~~~~~~~~~~~~~~~~~~~~~~
2847

2948

30-
Knot Resolver 5.x.y (202y-mm-dd)
49+
Knot Resolver 5.7.1 (2024-02-13)
3150
================================
3251

52+
Security
53+
--------
54+
- CVE-2023-50868: NSEC3 closest encloser proof can exhaust CPU
55+
* validator: lower the NSEC3 iteration limit (150 -> 50)
56+
* validator: similarly also limit excessive NSEC3 salt length
57+
* cache: limit the amount of work on SHA1 in NSEC3 aggressive cache
58+
* validator: limit the amount of work on SHA1 in NSEC3 proofs
59+
* validator: refuse to validate answers with more than 8 NSEC3 records
60+
61+
- CVE-2023-50387 "KeyTrap": DNSSEC verification complexity
62+
could be exploited to exhaust CPU resources and stall DNS resolvers.
63+
Solution boils down mainly to limiting crypto-validations per packet.
64+
65+
We would like to thank Elias Heftrig, Haya Schulmann, Niklas Vogel and Michael Waidner
66+
from the German National Research Center for Applied Cybersecurity ATHENE
67+
for bringing this vulnerability to our attention.
68+
3369
Improvements
3470
------------
3571
- update addresses of B.root-servers.net (!1478)

daemon/lua/kres-gen-30.lua

+3
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,8 @@ struct kr_query {
371371
struct kr_qflags forward_flags;
372372
uint32_t secret;
373373
uint32_t uid;
374+
int32_t vld_limit_crypto_remains;
375+
uint32_t vld_limit_uid;
374376
uint64_t creation_time_mono;
375377
uint64_t timestamp_mono;
376378
struct timeval timestamp;
@@ -389,6 +391,7 @@ struct kr_context {
389391
knot_rrset_t *upstream_opt_rr;
390392
trie_t *trust_anchors;
391393
trie_t *negative_anchors;
394+
int32_t vld_limit_crypto;
392395
struct kr_zonecut root_hints;
393396
struct kr_cache cache;
394397
unsigned int cache_rtt_tout_retry_interval;

daemon/lua/kres-gen-31.lua

+3
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,8 @@ struct kr_query {
371371
struct kr_qflags forward_flags;
372372
uint32_t secret;
373373
uint32_t uid;
374+
int32_t vld_limit_crypto_remains;
375+
uint32_t vld_limit_uid;
374376
uint64_t creation_time_mono;
375377
uint64_t timestamp_mono;
376378
struct timeval timestamp;
@@ -389,6 +391,7 @@ struct kr_context {
389391
knot_rrset_t *upstream_opt_rr;
390392
trie_t *trust_anchors;
391393
trie_t *negative_anchors;
394+
int32_t vld_limit_crypto;
392395
struct kr_zonecut root_hints;
393396
struct kr_cache cache;
394397
unsigned int cache_rtt_tout_retry_interval;

daemon/lua/kres-gen-32.lua

+3
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,8 @@ struct kr_query {
372372
struct kr_qflags forward_flags;
373373
uint32_t secret;
374374
uint32_t uid;
375+
int32_t vld_limit_crypto_remains;
376+
uint32_t vld_limit_uid;
375377
uint64_t creation_time_mono;
376378
uint64_t timestamp_mono;
377379
struct timeval timestamp;
@@ -390,6 +392,7 @@ struct kr_context {
390392
knot_rrset_t *upstream_opt_rr;
391393
trie_t *trust_anchors;
392394
trie_t *negative_anchors;
395+
int32_t vld_limit_crypto;
393396
struct kr_zonecut root_hints;
394397
struct kr_cache cache;
395398
unsigned int cache_rtt_tout_retry_interval;

lib/cache/api.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ static ssize_t stash_rrset(struct kr_cache *cache, const struct kr_query *qry,
512512
return kr_ok();
513513
}
514514
if (rr->type == KNOT_RRTYPE_NSEC3 && rr->rrs.count
515-
&& knot_nsec3_iters(rr->rrs.rdata) > KR_NSEC3_MAX_ITERATIONS) {
515+
&& kr_nsec3_limited_rdata(rr->rrs.rdata)) {
516516
/* This shouldn't happen often, thanks to downgrades during validation. */
517517
VERBOSE_MSG(qry, "=> skipping NSEC3 with too many iterations\n");
518518
return kr_ok();

lib/cache/nsec3.c

+15-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ static knot_db_val_t key_NSEC3_name(struct key *k, const knot_dname_t *name,
8484
.data = (uint8_t *)/*const-cast*/name,
8585
};
8686

87-
if (kr_fails_assert(nsec_p->libknot.iterations <= KR_NSEC3_MAX_ITERATIONS)) {
87+
if (kr_fails_assert(!kr_nsec3_limited_params(&nsec_p->libknot))) {
8888
/* This is mainly defensive; it shouldn't happen thanks to downgrades. */
8989
return VAL_EMPTY;
9090
}
@@ -272,8 +272,22 @@ int nsec3_encloser(struct key *k, struct answer *ans,
272272
const int zname_labels = knot_dname_labels(k->zname, NULL);
273273
int last_nxproven_labels = -1;
274274
const knot_dname_t *name = qry->sname;
275+
276+
/* Avoid doing too much work on SHA1; we might consider that a part of mitigating
277+
* CVE-2023-50868: NSEC3 closest encloser proof can exhaust CPU
278+
* As currently the code iterates from the longest name, we limit that.
279+
* Note that we don't want to limit too much, as the alternative usually includes
280+
* sending more queries upstream, which would come with nontrivial work, too.
281+
*/
282+
const int max_labels = zname_labels + kr_nsec3_max_depth(&ans->nsec_p.libknot);
283+
if (sname_labels > max_labels)
284+
VERBOSE_MSG(qry, "=> NSEC3 hashing partly skipped due to too long SNAME (CVE-2023-50868)\n");
285+
275286
for (int name_labels = sname_labels; name_labels >= zname_labels;
276287
--name_labels, name += 1 + name[0]) {
288+
if (name_labels > max_labels)
289+
continue; // avoid the hashing
290+
277291
/* Find a previous-or-equal NSEC3 in cache covering the name,
278292
* checking TTL etc. */
279293
const knot_db_val_t key = key_NSEC3_name(k, name, false, &ans->nsec_p);

lib/defines.h

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ static inline int KR_COLD kr_error(int x) {
5757
#define KR_COUNT_NO_NSADDR_LIMIT 5
5858
#define KR_CONSUME_FAIL_ROW_LIMIT 3 /* Maximum number of KR_STATE_FAIL in a row. */
5959

60+
#define KR_VLD_LIMIT_CRYPTO_DEFAULT 32 /**< default for struct kr_query::vld_limit_crypto */
61+
6062
/*
6163
* Defines.
6264
*/

lib/dnssec.c

+37-9
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ int kr_rrset_validate(kr_rrset_validation_ctx_t *vctx, knot_rrset_t *covered)
134134
memset(&vctx->rrs_counters, 0, sizeof(vctx->rrs_counters));
135135
for (unsigned i = 0; i < vctx->keys->rrs.count; ++i) {
136136
int ret = kr_rrset_validate_with_key(vctx, covered, i, NULL);
137-
if (ret == 0) {
137+
if (ret == 0 || ret == kr_error(E2BIG)) {
138138
return ret;
139139
}
140140
}
@@ -240,6 +240,29 @@ struct kr_svldr_ctx * kr_svldr_new_ctx(const knot_rrset_t *ds, knot_rrset_t *dns
240240
return NULL;
241241
}
242242

243+
/// Return if we want to afford yet another crypto-validation (and account it).
244+
static bool check_crypto_limit(const kr_rrset_validation_ctx_t *vctx)
245+
{
246+
if (vctx->limit_crypto_remains == NULL)
247+
return true; // no limiting
248+
if (*vctx->limit_crypto_remains > 0) {
249+
--*vctx->limit_crypto_remains;
250+
return true;
251+
}
252+
// We got over limit. There are optional actions to do.
253+
if (vctx->log_qry && kr_log_is_debug_qry(VALIDATOR, vctx->log_qry)) {
254+
auto_free char *name_str = kr_dname_text(vctx->zone_name);
255+
kr_log_q(vctx->log_qry, VALIDATOR,
256+
"expensive crypto limited, mitigating CVE-2023-50387, current zone: %s\n",
257+
name_str);
258+
}
259+
if (vctx->log_qry && vctx->log_qry->request) {
260+
kr_request_set_extended_error(vctx->log_qry->request, KNOT_EDNS_EDE_BOGUS,
261+
"EAIE: expensive crypto limited, mitigating CVE-2023-50387");
262+
}
263+
return false;
264+
}
265+
243266
static int kr_svldr_rrset_with_key(knot_rrset_t *rrs, const knot_rdataset_t *rrsigs,
244267
kr_rrset_validation_ctx_t *vctx, const kr_svldr_key_t *key)
245268
{
@@ -258,6 +281,8 @@ static int kr_svldr_rrset_with_key(knot_rrset_t *rrs, const knot_rdataset_t *rrs
258281
} else if (retv != 0) {
259282
continue;
260283
}
284+
if (!check_crypto_limit(vctx))
285+
return vctx->result = kr_error(E2BIG);
261286
// We only expect non-expanded wildcard records in input;
262287
// that also means we don't need to perform non-existence proofs.
263288
const int trim_labels = (val_flgs & FLG_WILDCARD_EXPANSION) ? 1 : 0;
@@ -282,7 +307,7 @@ int kr_svldr_rrset(knot_rrset_t *rrs, const knot_rdataset_t *rrsigs,
282307
}
283308
for (ssize_t i = 0; i < ctx->keys.len; ++i) {
284309
kr_svldr_rrset_with_key(rrs, rrsigs, &ctx->vctx, &ctx->keys.at[i]);
285-
if (ctx->vctx.result == 0)
310+
if (ctx->vctx.result == 0 || ctx->vctx.result == kr_error(E2BIG))
286311
break;
287312
}
288313
return ctx->vctx.result;
@@ -356,9 +381,8 @@ static int kr_rrset_validate_with_key(kr_rrset_validation_ctx_t *vctx,
356381
int retv = validate_rrsig_rr(&val_flgs, covered_labels, rdata_j,
357382
key_alg, keytag, vctx);
358383
if (retv == kr_error(EAGAIN)) {
359-
kr_dnssec_key_free(&created_key);
360384
vctx->result = retv;
361-
return retv;
385+
goto finish;
362386
} else if (retv != 0) {
363387
continue;
364388
}
@@ -368,6 +392,10 @@ static int kr_rrset_validate_with_key(kr_rrset_validation_ctx_t *vctx,
368392
break;
369393
}
370394
}
395+
if (!check_crypto_limit(vctx)) {
396+
vctx->result = kr_error(E2BIG);
397+
goto finish;
398+
}
371399
if (kr_check_signature(rdata_j, key, covered, trim_labels) != 0) {
372400
vctx->rrs_counters.crypto_invalid++;
373401
continue;
@@ -392,15 +420,15 @@ static int kr_rrset_validate_with_key(kr_rrset_validation_ctx_t *vctx,
392420

393421
trim_ttl(covered, rdata_j, vctx);
394422

395-
kr_dnssec_key_free(&created_key);
396-
vctx->result = kr_ok();
397423
kr_rank_set(&vctx->rrs->at[i]->rank, KR_RANK_SECURE); /* upgrade from bogus */
398-
return vctx->result;
424+
vctx->result = kr_ok();
425+
goto finish;
399426
}
400427
}
401428
/* No applicable key found, cannot be validated. */
402-
kr_dnssec_key_free(&created_key);
403429
vctx->result = kr_error(ENOENT);
430+
finish:
431+
kr_dnssec_key_free(&created_key);
404432
return vctx->result;
405433
}
406434

@@ -448,7 +476,7 @@ int kr_dnskeys_trusted(kr_rrset_validation_ctx_t *vctx, const knot_rdataset_t *s
448476
if (ret == 0)
449477
ret = kr_svldr_rrset_with_key(keys, sigs, vctx, &key);
450478
svldr_key_del(&key);
451-
if (ret == 0) {
479+
if (ret == 0 || ret == kr_error(E2BIG)) {
452480
kr_assert(vctx->result == 0);
453481
return vctx->result;
454482
}

lib/dnssec.h

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ struct kr_rrset_validation_ctx {
4444
uint32_t flags; /*!< Output - Flags. */
4545
uint32_t err_cnt; /*!< Output - Number of validation failures. */
4646
uint32_t cname_norrsig_cnt; /*!< Output - Number of CNAMEs missing RRSIGs. */
47+
int32_t *limit_crypto_remains; /*!< Optional pointer to struct kr_query::vld_limit_crypto_remains */
4748

4849
/** Validation result: kr_error() code.
4950
*

lib/dnssec/nsec3.c

+14-2
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ static int hash_name(dnssec_binary_t *hash, const dnssec_nsec3_params_t *params,
7171
return kr_error(EINVAL);
7272
if (!name)
7373
return kr_error(EINVAL);
74-
if (kr_fails_assert(params->iterations <= KR_NSEC3_MAX_ITERATIONS)) {
74+
if (kr_fails_assert(!kr_nsec3_limited_params(params))) {
7575
/* This if is mainly defensive; it shouldn't happen. */
7676
return kr_error(EINVAL);
7777
}
@@ -146,6 +146,18 @@ static int closest_encloser_match(int *flags, const knot_rrset_t *nsec3,
146146
const knot_dname_t *encloser = knot_wire_next_label(name, NULL);
147147
*skipped = 1;
148148

149+
/* Avoid doing too much work on SHA1, mitigating:
150+
* CVE-2023-50868: NSEC3 closest encloser proof can exhaust CPU
151+
* We log nothing here; it wouldn't be easy from this place
152+
* and huge SNAME should be suspicious on its own.
153+
*/
154+
const int max_labels = knot_dname_labels(nsec3->owner, NULL) - 1
155+
+ kr_nsec3_max_depth(&params);
156+
for (int l = knot_dname_labels(encloser, NULL); l > max_labels; --l) {
157+
encloser = knot_wire_next_label(encloser, NULL);
158+
++(*skipped);
159+
}
160+
149161
while(encloser) {
150162
ret = hash_name(&name_hash, &params, encloser);
151163
if (ret != 0)
@@ -565,7 +577,7 @@ int kr_nsec3_wildcard_answer_response_check(const knot_pkt_t *pkt, knot_section_
565577
const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
566578
if (rrset->type != KNOT_RRTYPE_NSEC3)
567579
continue;
568-
if (knot_nsec3_iters(rrset->rrs.rdata) > KR_NSEC3_MAX_ITERATIONS) {
580+
if (kr_nsec3_limited_rdata(rrset->rrs.rdata)) {
569581
/* Avoid hashing with too many iterations.
570582
* If we get here, the `sname` wildcard probably ends up bogus,
571583
* but it gets downgraded to KR_RANK_INSECURE when validator

lib/dnssec/nsec3.h

+41-8
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,51 @@
55
#pragma once
66

77
#include <libknot/packet/pkt.h>
8+
#include <libknot/rrtype/nsec3.h>
9+
#include <libdnssec/nsec.h>
10+
11+
12+
static inline unsigned int kr_nsec3_price(unsigned int iterations, unsigned int salt_len)
13+
{
14+
// SHA1 works on 64-byte chunks.
15+
// On iterating we hash the salt + 20 bytes of the previous hash.
16+
int chunks_per_iter = (20 + salt_len - 1) / 64 + 1;
17+
return (iterations + 1) * chunks_per_iter;
18+
}
819

920
/** High numbers in NSEC3 iterations don't really help security
1021
*
11-
* ...so we avoid doing all the work. The value is a current compromise;
12-
* zones shooting over get downgraded to insecure status.
22+
* ...so we avoid doing all the work. The limit is a current compromise;
23+
* answers using NSEC3 over kr_nsec3_limited* get downgraded to insecure status.
1324
*
14-
* Original restriction wasn't that strict:
15-
https://datatracker.ietf.org/doc/html/rfc5155#section-10.3
16-
* but there is discussion about officially lowering the limits:
17-
https://tools.ietf.org/id/draft-hardaker-dnsop-nsec3-guidance-02.html#section-2.3
25+
https://datatracker.ietf.org/doc/html/rfc9276#name-recommendation-for-validati
1826
*/
19-
#define KR_NSEC3_MAX_ITERATIONS 150
27+
static inline bool kr_nsec3_limited(unsigned int iterations, unsigned int salt_len)
28+
{
29+
const int MAX_ITERATIONS = 50; // limit with short salt length
30+
return kr_nsec3_price(iterations, salt_len) > MAX_ITERATIONS + 1;
31+
}
32+
static inline bool kr_nsec3_limited_rdata(const knot_rdata_t *rd)
33+
{
34+
return kr_nsec3_limited(knot_nsec3_iters(rd), knot_nsec3_salt_len(rd));
35+
}
36+
static inline bool kr_nsec3_limited_params(const dnssec_nsec3_params_t *params)
37+
{
38+
return kr_nsec3_limited(params->iterations, params->salt.size);
39+
}
40+
41+
/** Return limit on NSEC3 depth. The point is to avoid doing too much work on SHA1.
42+
*
43+
* CVE-2023-50868: NSEC3 closest encloser proof can exhaust CPU
44+
*
45+
* 128 is chosen so that zones with good NSEC3 parameters (giving _price() == 1)
46+
* won't be limited in any way. Performance doesn't seem too bad with that either.
47+
*/
48+
static inline int kr_nsec3_max_depth(const dnssec_nsec3_params_t *params)
49+
{
50+
return 128 / kr_nsec3_price(params->iterations, params->salt.size);
51+
}
52+
2053

2154
/**
2255
* Name error response check (RFC5155 7.2.2).
@@ -39,7 +72,7 @@ int kr_nsec3_name_error_response_check(const knot_pkt_t *pkt, knot_section_t sec
3972
* KNOT_ERANGE - NSEC3 RR that covers a wildcard
4073
* has been found, but has opt-out flag set;
4174
* otherwise - error.
42-
* Records over KR_NSEC3_MAX_ITERATIONS are skipped, so you probably get kr_error(ENOENT).
75+
* Too expensive NSEC3 records are skipped, so you probably get kr_error(ENOENT).
4376
*/
4477
int kr_nsec3_wildcard_answer_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
4578
const knot_dname_t *sname, int trim_to_next);

0 commit comments

Comments
 (0)