dnsmasq/SOURCES/dnsmasq-2.90-CVE-2023-50387-CVE-2023-50868.patch

2319 lines
82 KiB
Diff
Raw Normal View History

2024-03-14 18:54:22 +00:00
commit 5eab3c6f13562230f1eeab3788585f73ffbf469d
Author: Tomas Korbar <tkorbar@redhat.com>
Date: Wed Feb 28 16:51:17 2024 +0100
Fix CVE 2023-50387 and CVE 2023-50868
Add limits on the resources used to do DNSSEC validation.
DNSSEC introduces a potential CPU DoS, because a crafted domain
can force a validator to a large number of cryptographic
operations whilst attempting to do validation. When using TCP
transport a DNSKEY RRset contain thousands of members and any
RRset can have thousands of signatures. The potential number
of signature validations to follow the RFC for validation
for one RRset is the cross product of the keys and signatures,
so millions. In practice, the actual numbers are much lower,
so attacks can be mitigated by limiting the amount of
cryptographic "work" to a much lower amount. The actual
limits are number a signature validation fails per RRset(20),
number of signature validations and hash computations
per query(200), number of sub-queries to fetch DS and DNSKEY
RRsets per query(40), and the number of iterations in a
NSEC3 record(150). These values are sensible, but there is, as yet,
no standardisation on the values for a "conforming" domain, so a
new option --dnssec-limit is provided should they need to be altered.
The algorithm to validate DS records has also been altered to reduce
the maximum work from cross product of the number of DS records and
number of DNSKEYs to the cross product of the number of DS records
and supported DS digest types. As the number of DS digest types
is in single figures, this reduces the exposure.
Credit is due to Elias Heftrig, Haya Schulmann, Niklas Vogel,
and Michael Waidner from the German National Research Center for
Applied Cybersecurity ATHENE for finding this vulnerability.
CVE 2023-50387 and CVE 2023-50868 apply.
Note that the is a security vulnerablity only when DNSSEC validation
is enabled.
diff --git a/man/dnsmasq.8 b/man/dnsmasq.8
index d80cad5..db94a99 100644
--- a/man/dnsmasq.8
+++ b/man/dnsmasq.8
@@ -762,6 +762,15 @@ Copy the DNSSEC Authenticated Data bit from upstream servers to downstream clien
alternative to having dnsmasq validate DNSSEC, but it depends on the security of the network between
dnsmasq and the upstream servers, and the trustworthiness of the upstream servers.
.TP
+.B --dnssec-limits=<limit>[,<limit>.......]
+Override the default resource limits applied to DNSSEC validation. Cryptographic operations are expensive and crafted domains
+can DoS a DNSSEC validator by forcing it to do hundreds of thousands of such operations. To avoid this, the dnsmasq validation code
+applies limits on how much work will be expended in validation. If any of the limits are exceeded, the validation will fail and the
+domain treated as BOGUS. There are four limits, in order(default values in parens): number a signature validation fails per RRset(20), number of signature validations and
+hash computations per query(200), number of sub-queries to fetch DS and DNSKEY RRsets per query(40), and the number of iterations in a NSEC3 record(150).
+The maximum values reached during validation are stored, and dumped as part of the stats generated by SIGUSR1. Supplying a limit value of 0 leaves the default in place, so
+\fB--dnssec-limits=0,0,20\fP sets the number of sub-queries to 20 whilst leaving the other limits at default values.
+.TP
.B --dnssec-debug
Set debugging mode for the DNSSEC validation, set the Checking Disabled bit on upstream queries,
and don't convert replies which do not validate to responses with
diff --git a/src/config.h b/src/config.h
index 3878ba4..8d7bd79 100644
--- a/src/config.h
+++ b/src/config.h
@@ -22,7 +22,10 @@
#define EDNS_PKTSZ 1232 /* default max EDNS.0 UDP packet from from /dnsflagday.net/2020 */
#define SAFE_PKTSZ 1280 /* "go anywhere" UDP packet size */
#define KEYBLOCK_LEN 40 /* choose to minimise fragmentation when storing DNSSEC keys */
-#define DNSSEC_WORK 50 /* Max number of queries to validate one question */
+#define DNSSEC_LIMIT_WORK 40 /* Max number of queries to validate one question */
+#define DNSSEC_LIMIT_SIG_FAIL 20 /* Number of signature that can fail to validate in one answer */
+#define DNSSEC_LIMIT_CRYPTO 200 /* max no. of crypto operations to validate one query. */
+#define DNSSEC_LIMIT_NSEC3_ITERS 150 /* Max. number if iterations allowed in NSEC3 record. */
#define TIMEOUT 10 /* drop UDP queries after TIMEOUT seconds */
#define FORWARD_TEST 50 /* try all servers every 50 queries */
#define FORWARD_TIME 20 /* or 20 seconds */
diff --git a/src/dnsmasq.h b/src/dnsmasq.h
index 711ffd3..cf1c9c4 100644
--- a/src/dnsmasq.h
+++ b/src/dnsmasq.h
@@ -626,15 +626,30 @@ struct hostsfile {
/* DNSSEC status values. */
-#define STAT_SECURE 1
-#define STAT_INSECURE 2
-#define STAT_BOGUS 3
-#define STAT_NEED_DS 4
-#define STAT_NEED_KEY 5
-#define STAT_TRUNCATED 6
-#define STAT_SECURE_WILDCARD 7
-#define STAT_OK 8
-#define STAT_ABANDONED 9
+#define STAT_SECURE 0x10000
+#define STAT_INSECURE 0x20000
+#define STAT_BOGUS 0x30000
+#define STAT_NEED_DS 0x40000
+#define STAT_NEED_KEY 0x50000
+#define STAT_TRUNCATED 0x60000
+#define STAT_SECURE_WILDCARD 0x70000
+#define STAT_OK 0x80000
+#define STAT_ABANDONED 0x90000
+
+#define DNSSEC_FAIL_NYV 0x0001 /* key not yet valid */
+#define DNSSEC_FAIL_EXP 0x0002 /* key expired */
+#define DNSSEC_FAIL_INDET 0x0004 /* indetermined */
+#define DNSSEC_FAIL_NOKEYSUP 0x0008 /* no supported key algo. */
+#define DNSSEC_FAIL_NOSIG 0x0010 /* No RRsigs */
+#define DNSSEC_FAIL_NOZONE 0x0020 /* No Zone bit set */
+#define DNSSEC_FAIL_NONSEC 0x0040 /* No NSEC */
+#define DNSSEC_FAIL_NODSSUP 0x0080 /* no supported DS algo. */
+#define DNSSEC_FAIL_NOKEY 0x0100 /* no DNSKEY */
+#define DNSSEC_FAIL_NSEC3_ITERS 0x0200 /* too many iterations in NSEC3 */
+#define DNSSEC_FAIL_BADPACKET 0x0400 /* bad packet */
+#define DNSSEC_FAIL_WORK 0x0800 /* too much crypto */
+
+#define STAT_ISEQUAL(a, b) (((a) & 0xffff0000) == (b))
#define FREC_NOREBIND 1
#define FREC_CHECKING_DISABLED 2
@@ -666,7 +681,7 @@ struct frec {
time_t time;
unsigned char *hash[HASH_SIZE];
#ifdef HAVE_DNSSEC
- int class, work_counter;
+ int class, work_counter, validate_counter;
struct blockdata *stash; /* Saved reply, whilst we validate */
size_t stash_len;
struct frec *dependent; /* Query awaiting internally-generated DNSKEY or DS query */
@@ -702,6 +717,12 @@ struct frec {
#define LEASE_TA 64 /* IPv6 temporary lease */
#define LEASE_HAVE_HWADDR 128 /* Have set hwaddress */
+#define LIMIT_SIG_FAIL 0
+#define LIMIT_CRYPTO 1
+#define LIMIT_WORK 2
+#define LIMIT_NSEC3_ITERS 3
+#define LIMIT_MAX 4
+
struct dhcp_lease {
int clid_len; /* length of client identifier */
unsigned char *clid; /* clientid */
@@ -1055,6 +1076,7 @@ extern struct daemon {
struct ds_config *ds;
int dnssec_no_time_check;
int back_to_the_future;
+ int limit[LIMIT_MAX];
char *timestamp_file;
#endif
@@ -1210,10 +1232,12 @@ int in_zone(struct auth_zone *zone, char *name, char **cut);
/* dnssec.c */
size_t dnssec_generate_query(struct dns_header *header, unsigned char *end, char *name, int class, int type, union mysockaddr *addr, int edns_pktsz);
-int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int class);
-int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int class);
+int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, char *name,
+ char *keyname, int class, int *validate_count);
+int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name,
+ char *keyname, int class, int *validate_count);
int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int *class,
- int check_unsigned, int *neganswer, int *nons);
+ int check_unsigned, int *neganswer, int *nons, int *nsec_ttl, int *validate_count);
int dnskey_keytag(int alg, int flags, unsigned char *key, int keylen);
size_t filter_rrsigs(struct dns_header *header, size_t plen);
int setup_timestamp(void);
@@ -1602,7 +1626,7 @@ int do_poll(int timeout);
/* rrfilter.c */
size_t rrfilter(struct dns_header *header, size_t plen, int mode);
-u16 *rrfilter_desc(int type);
+short *rrfilter_desc(int type);
int expand_workspace(unsigned char ***wkspc, int *szp, int new);
/* edns0.c */
diff --git a/src/dnssec.c b/src/dnssec.c
index b2dda1b..fd6e722 100644
--- a/src/dnssec.c
+++ b/src/dnssec.c
@@ -186,10 +186,8 @@ int setup_timestamp(void)
}
/* Check whether today/now is between date_start and date_end */
-static int check_date_range(u32 date_start, u32 date_end)
+static int is_check_date(unsigned long curtime)
{
- unsigned long curtime = time(0);
-
/* Checking timestamps may be temporarily disabled */
/* If the current time if _before_ the timestamp
@@ -211,15 +209,10 @@ static int check_date_range(u32 date_start, u32 date_end)
queue_event(EVENT_RELOAD); /* purge cache */
}
- if (daemon->back_to_the_future == 0)
- return 1;
+ return daemon->back_to_the_future;
}
- else if (daemon->dnssec_no_time_check)
- return 1;
-
- /* We must explicitly check against wanted values, because of SERIAL_UNDEF */
- return serial_compare_32(curtime, date_start) == SERIAL_GT
- && serial_compare_32(curtime, date_end) == SERIAL_LT;
+ else
+ return !daemon->dnssec_no_time_check;
}
/* Return bytes of canonicalised rrdata one by one.
@@ -232,7 +225,7 @@ static int check_date_range(u32 date_start, u32 date_end)
On returning 0, the end has been reached.
*/
struct rdata_state {
- u16 *desc;
+ short *desc;
size_t c;
unsigned char *end, *ip, *op;
char *buff;
@@ -253,7 +246,7 @@ static int get_rdata(struct dns_header *header, size_t plen, struct rdata_state
{
d = *(state->desc);
- if (d == (u16)-1)
+ if (d == -1)
{
/* all the bytes to the end. */
if ((state->c = state->end - state->ip) != 0)
@@ -301,7 +294,7 @@ static int get_rdata(struct dns_header *header, size_t plen, struct rdata_state
/* Bubble sort the RRset into the canonical order. */
-static int sort_rrset(struct dns_header *header, size_t plen, u16 *rr_desc, int rrsetidx,
+static int sort_rrset(struct dns_header *header, size_t plen, short *rr_desc, int rrsetidx,
unsigned char **rrset, char *buff1, char *buff2)
{
int swap, i, j;
@@ -333,37 +326,64 @@ static int sort_rrset(struct dns_header *header, size_t plen, u16 *rr_desc, int
if (!CHECK_LEN(header, state2.ip, plen, rdlen2))
return rrsetidx; /* short packet */
state2.end = state2.ip + rdlen2;
-
- while (1)
+
+ /* If the RR has no names in it then canonicalisation
+ is the identity function and we can compare
+ the RRs directly. If not we compare the
+ canonicalised RRs one byte at a time. */
+ if (*rr_desc == -1)
{
- int ok1, ok2;
+ int rdmin = rdlen1 > rdlen2 ? rdlen2 : rdlen1;
+ int cmp = memcmp(state1.ip, state2.ip, rdmin);
- ok1 = get_rdata(header, plen, &state1);
- ok2 = get_rdata(header, plen, &state2);
-
- if (!ok1 && !ok2)
+ if (cmp > 0 || (cmp == 0 && rdlen1 > rdmin))
+ {
+ unsigned char *tmp = rrset[i+1];
+ rrset[i+1] = rrset[i];
+ rrset[i] = tmp;
+ swap = 1;
+ }
+ else if (cmp == 0 && (rdlen1 == rdlen2))
{
/* Two RRs are equal, remove one copy. RFC 4034, para 6.3 */
for (j = i+1; j < rrsetidx-1; j++)
rrset[j] = rrset[j+1];
rrsetidx--;
i--;
- break;
}
- else if (ok1 && (!ok2 || *state1.op > *state2.op))
- {
- unsigned char *tmp = rrset[i+1];
- rrset[i+1] = rrset[i];
- rrset[i] = tmp;
- swap = 1;
- break;
- }
- else if (ok2 && (!ok1 || *state2.op > *state1.op))
- break;
-
- /* arrive here when bytes are equal, go round the loop again
- and compare the next ones. */
}
+ else
+ /* Comparing canonicalised RRs, byte-at-a-time. */
+ while (1)
+ {
+ int ok1, ok2;
+
+ ok1 = get_rdata(header, plen, &state1);
+ ok2 = get_rdata(header, plen, &state2);
+
+ if (!ok1 && !ok2)
+ {
+ /* Two RRs are equal, remove one copy. RFC 4034, para 6.3 */
+ for (j = i+1; j < rrsetidx-1; j++)
+ rrset[j] = rrset[j+1];
+ rrsetidx--;
+ i--;
+ break;
+ }
+ else if (ok1 && (!ok2 || *state1.op > *state2.op))
+ {
+ unsigned char *tmp = rrset[i+1];
+ rrset[i+1] = rrset[i];
+ rrset[i] = tmp;
+ swap = 1;
+ break;
+ }
+ else if (ok2 && (!ok1 || *state2.op > *state1.op))
+ break;
+
+ /* arrive here when bytes are equal, go round the loop again
+ and compare the next ones. */
+ }
}
} while (swap);
@@ -383,7 +403,7 @@ static int explore_rrset(struct dns_header *header, size_t plen, int class, int
int gotkey = 0;
if (!(p = skip_questions(header, plen)))
- return STAT_BOGUS;
+ return 0;
/* look for RRSIGs for this RRset and get pointers to each RR in the set. */
for (rrsetidx = 0, sigidx = 0, j = ntohs(header->ancount) + ntohs(header->nscount);
@@ -395,14 +415,14 @@ static int explore_rrset(struct dns_header *header, size_t plen, int class, int
pstart = p;
if (!(res = extract_name(header, plen, &p, name, 0, 10)))
- return STAT_BOGUS; /* bad packet */
+ return 0; /* bad packet */
GETSHORT(stype, p);
GETSHORT(sclass, p);
- p += 4; /* TTL */
-
+
pdata = p;
+ p += 4; /* TTL */
GETSHORT(rdlen, p);
if (!CHECK_LEN(header, p, plen, rdlen))
@@ -465,7 +485,7 @@ static int explore_rrset(struct dns_header *header, size_t plen, int class, int
sigs[sigidx++] = pdata;
}
- p = pdata + 2; /* restore for ADD_RDLEN */
+ p = pdata + 6; /* restore for ADD_RDLEN */
}
}
@@ -479,12 +499,24 @@ static int explore_rrset(struct dns_header *header, size_t plen, int class, int
return 1;
}
+int dec_counter(int *counter, char *message)
+{
+ if ((*counter)-- == 0)
+ {
+ my_syslog(LOG_WARNING, "limit exceeded: %s", message ? message : _("per-query crypto work"));
+ return 1;
+ }
+
+ return 0;
+}
+
/* Validate a single RRset (class, type, name) in the supplied DNS reply
Return code:
STAT_SECURE if it validates.
STAT_SECURE_WILDCARD if it validates and is the result of wildcard expansion.
(In this case *wildcard_out points to the "body" of the wildcard within name.)
STAT_BOGUS signature is wrong, bad packet.
+ STAT_ABANDONED validation abandoned do to excess resource usage.
STAT_NEED_KEY need DNSKEY to complete validation (name is returned in keyname)
STAT_NEED_DS need DS to complete validation (name is returned in keyname)
@@ -494,16 +526,23 @@ static int explore_rrset(struct dns_header *header, size_t plen, int class, int
Name is unchanged on exit. keyname is used as workspace and trashed.
Call explore_rrset first to find and count RRs and sigs.
+
+ ttl_out is the floor on TTL, based on TTL and orig_ttl and expiration of sig used to validate.
*/
static int validate_rrset(time_t now, struct dns_header *header, size_t plen, int class, int type, int sigidx, int rrsetidx,
- char *name, char *keyname, char **wildcard_out, struct blockdata *key, int keylen, int algo_in, int keytag_in)
+ char *name, char *keyname, char **wildcard_out, struct blockdata *key, int keylen,
+ int algo_in, int keytag_in, unsigned long *ttl_out, int *validate_counter)
{
unsigned char *p;
- int rdlen, j, name_labels, algo, labels, orig_ttl, key_tag;
+ int rdlen, j, name_labels, algo, labels, key_tag, sig_fail_cnt;
struct crec *crecp = NULL;
- u16 *rr_desc = rrfilter_desc(type);
- u32 sig_expiration, sig_inception
-;
+ short *rr_desc = rrfilter_desc(type);
+ u32 sig_expiration, sig_inception;
+ int failflags = DNSSEC_FAIL_NOSIG | DNSSEC_FAIL_NYV | DNSSEC_FAIL_EXP | DNSSEC_FAIL_NOKEYSUP;
+
+ unsigned long curtime = time(0);
+ int time_check = is_check_date(curtime);
+
if (wildcard_out)
*wildcard_out = NULL;
@@ -515,16 +554,19 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
rrsetidx = sort_rrset(header, plen, rr_desc, rrsetidx, rrset, daemon->workspacename, keyname);
/* Now try all the sigs to try and find one which validates */
- for (j = 0; j <sigidx; j++)
+ for (sig_fail_cnt = daemon->limit[LIMIT_SIG_FAIL], j = 0; j <sigidx; j++)
{
unsigned char *psav, *sig, *digest;
int i, wire_len, sig_len;
const struct nettle_hash *hash;
void *ctx;
char *name_start;
- u32 nsigttl;
+ u32 nsigttl, ttl, orig_ttl;
+
+ failflags &= ~DNSSEC_FAIL_NOSIG;
p = sigs[j];
+ GETLONG(ttl, p);
GETSHORT(rdlen, p); /* rdlen >= 18 checked previously */
psav = p;
@@ -539,16 +581,47 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
if (!extract_name(header, plen, &p, keyname, 1, 0))
return STAT_BOGUS;
- if (!check_date_range(sig_inception, sig_expiration) ||
- labels > name_labels ||
- !(hash = hash_find(algo_digest_name(algo))) ||
+ if (!time_check)
+ failflags &= ~(DNSSEC_FAIL_NYV | DNSSEC_FAIL_EXP);
+ else
+ {
+ /* We must explicitly check against wanted values, because of SERIAL_UNDEF */
+ if (serial_compare_32(curtime, sig_inception) == SERIAL_LT)
+ continue;
+ else
+ failflags &= ~DNSSEC_FAIL_NYV;
+
+ if (serial_compare_32(curtime, sig_expiration) == SERIAL_GT)
+ continue;
+ else
+ failflags &= ~DNSSEC_FAIL_EXP;
+ }
+
+ if (!(hash = hash_find(algo_digest_name(algo))))
+ continue;
+ else
+ failflags &= ~DNSSEC_FAIL_NOKEYSUP;
+
+ if (labels > name_labels ||
!hash_init(hash, &ctx, &digest))
continue;
/* OK, we have the signature record, see if the relevant DNSKEY is in the cache. */
if (!key && !(crecp = cache_find_by_name(NULL, keyname, now, F_DNSKEY)))
return STAT_NEED_KEY;
-
+
+ if (ttl_out)
+ {
+ /* 4035 5.3.3 rules on TTLs */
+ if (orig_ttl < ttl)
+ ttl = orig_ttl;
+
+ if (time_check && difftime(sig_expiration, curtime) < ttl)
+ ttl = difftime(sig_expiration, curtime);
+
+ *ttl_out = ttl;
+ }
+
sig = p;
sig_len = rdlen - (p - psav);
@@ -559,7 +632,7 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
hash->update(ctx, (unsigned int)wire_len, (unsigned char*)keyname);
from_wire(keyname);
-#define RRBUFLEN 300 /* Most RRs are smaller than this. */
+#define RRBUFLEN 128 /* Most RRs are smaller than this. */
for (i = 0; i < rrsetidx; ++i)
{
@@ -597,63 +670,85 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
hash->update(ctx, (unsigned int)wire_len, (unsigned char *)name_start);
hash->update(ctx, 4, p); /* class and type */
hash->update(ctx, 4, (unsigned char *)&nsigttl);
-
- p += 8; /* skip class, type, ttl */
+
+ p += 8; /* skip type, class, ttl */
GETSHORT(rdlen, p);
if (!CHECK_LEN(header, p, plen, rdlen))
return STAT_BOGUS;
-
- /* canonicalise rdata and calculate length of same, use
- name buffer as workspace for get_rdata. */
- state.ip = p;
- state.op = NULL;
- state.desc = rr_desc;
- state.buff = name;
- state.end = p + rdlen;
-
- for (j = 0; get_rdata(header, plen, &state); j++)
- if (j < RRBUFLEN)
- rrbuf[j] = *state.op;
- len = htons((u16)j);
- hash->update(ctx, 2, (unsigned char *)&len);
-
- /* If the RR is shorter than RRBUFLEN (most of them, in practice)
- then we can just digest it now. If it exceeds RRBUFLEN we have to
- go back to the start and do it in chunks. */
- if (j >= RRBUFLEN)
+ /* Optimisation for RR types which need no cannonicalisation.
+ This includes DNSKEY DS NSEC and NSEC3, which are also long, so
+ it saves lots of calls to get_rdata, and avoids the pessimal
+ segmented insertion, even with a small rrbuf[].
+
+ If canonicalisation is not needed, a simple insertion into the hash works.
+ */
+ if (*rr_desc == -1)
+ {
+ len = htons(rdlen);
+ hash->update(ctx, 2, (unsigned char *)&len);
+ hash->update(ctx, rdlen, p);
+ }
+ else
{
+ /* canonicalise rdata and calculate length of same, use
+ name buffer as workspace for get_rdata. */
state.ip = p;
state.op = NULL;
state.desc = rr_desc;
-
+ state.buff = name;
+ state.end = p + rdlen;
+
for (j = 0; get_rdata(header, plen, &state); j++)
+ if (j < RRBUFLEN)
+ rrbuf[j] = *state.op;
+
+ len = htons((u16)j);
+ hash->update(ctx, 2, (unsigned char *)&len);
+
+ /* If the RR is shorter than RRBUFLEN (most of them, in practice)
+ then we can just digest it now. If it exceeds RRBUFLEN we have to
+ go back to the start and do it in chunks. */
+ if (j >= RRBUFLEN)
{
- rrbuf[j] = *state.op;
-
- if (j == RRBUFLEN - 1)
- {
- hash->update(ctx, RRBUFLEN, rrbuf);
- j = -1;
- }
+ state.ip = p;
+ state.op = NULL;
+ state.desc = rr_desc;
+
+ for (j = 0; get_rdata(header, plen, &state); j++)
+ {
+ rrbuf[j] = *state.op;
+
+ if (j == RRBUFLEN - 1)
+ {
+ hash->update(ctx, RRBUFLEN, rrbuf);
+ j = -1;
+ }
+ }
}
+
+ if (j != 0)
+ hash->update(ctx, j, rrbuf);
}
-
- if (j != 0)
- hash->update(ctx, j, rrbuf);
}
hash->digest(ctx, hash->digest_size, digest);
/* namebuff used for workspace above, restore to leave unchanged on exit */
p = (unsigned char*)(rrset[0]);
- extract_name(header, plen, &p, name, 1, 0);
+ if (!extract_name(header, plen, &p, name, 1, 0))
+ return STAT_BOGUS;
if (key)
{
- if (algo_in == algo && keytag_in == key_tag &&
- verify(key, keylen, sig, sig_len, digest, hash->digest_size, algo))
- return STAT_SECURE;
+ if (algo_in == algo && keytag_in == key_tag)
+ {
+ if (dec_counter(validate_counter, NULL))
+ return STAT_ABANDONED;
+
+ if (verify(key, keylen, sig, sig_len, digest, hash->digest_size, algo))
+ return STAT_SECURE;
+ }
}
else
{
@@ -661,13 +756,25 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
for (; crecp; crecp = cache_find_by_name(crecp, keyname, now, F_DNSKEY))
if (crecp->addr.key.algo == algo &&
crecp->addr.key.keytag == key_tag &&
- crecp->uid == (unsigned int)class &&
- verify(crecp->addr.key.keydata, crecp->addr.key.keylen, sig, sig_len, digest, hash->digest_size, algo))
- return (labels < name_labels) ? STAT_SECURE_WILDCARD : STAT_SECURE;
+ crecp->uid == (unsigned int)class)
+ {
+ if (dec_counter(validate_counter, NULL))
+ return STAT_ABANDONED;
+
+ if (verify(crecp->addr.key.keydata, crecp->addr.key.keylen, sig, sig_len, digest, hash->digest_size, algo))
+ return (labels < name_labels) ? STAT_SECURE_WILDCARD : STAT_SECURE;
+
+ /* An attacker can waste a lot of our CPU by setting up a giant DNSKEY RRSET full of failing
+ keys, all of which we have to try. Since many failing keys is not likely for
+ a legitimate domain, set a limit on how many can fail. */
+ if (dec_counter(&sig_fail_cnt, _("per-RRSet signature fails")))
+ return STAT_ABANDONED;
+ }
}
}
- return STAT_BOGUS;
+ /* If we reach this point, no verifying key was found */
+ return STAT_BOGUS | failflags | DNSSEC_FAIL_NOKEY;
}
@@ -677,36 +784,45 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
STAT_OK Done, key(s) in cache.
STAT_BOGUS No DNSKEYs found, which can be validated with DS,
or self-sign for DNSKEY RRset is not valid, bad packet.
+ STAT_ABANDONED resource exhaustion.
STAT_NEED_DS DS records to validate a key not found, name in keyname
- STAT_NEED_KEY DNSKEY records to validate a key not found, name in keyname
*/
-int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int class)
+int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, char *name,
+ char *keyname, int class, int *validate_counter)
{
- unsigned char *psave, *p = (unsigned char *)(header+1);
+ unsigned char *psave, *p = (unsigned char *)(header+1), *keyaddr;
struct crec *crecp, *recp1;
- int rc, j, qtype, qclass, ttl, rdlen, flags, algo, valid, keytag;
- struct blockdata *key;
+ int rc, j, qtype, qclass, rdlen, flags, algo, keytag, sigcnt, rrcnt;
+ unsigned long ttl, sig_ttl;
struct all_addr a;
+ int failflags = DNSSEC_FAIL_NODSSUP | DNSSEC_FAIL_NOZONE;
+ char valid_digest[255];
+ static unsigned char **cached_digest;
+ static size_t cached_digest_size = 0;
- if (ntohs(header->qdcount) != 1 ||
- !extract_name(header, plen, &p, name, 1, 4))
- return STAT_BOGUS;
+ if (ntohs(header->qdcount) != 1 || RCODE(header) != NOERROR || !extract_name(header, plen, &p, name, 1, 4))
+ return STAT_BOGUS | DNSSEC_FAIL_NOKEY;
GETSHORT(qtype, p);
GETSHORT(qclass, p);
- if (qtype != T_DNSKEY || qclass != class || ntohs(header->ancount) == 0)
- return STAT_BOGUS;
+ if (qtype != T_DNSKEY || qclass != class ||
+ !explore_rrset(header, plen, class, T_DNSKEY, name, keyname, &sigcnt, &rrcnt) ||
+ rrcnt == 0)
+ return STAT_BOGUS | DNSSEC_FAIL_NOKEY;
+ if (sigcnt == 0)
+ return STAT_BOGUS | DNSSEC_FAIL_NOSIG;
+
/* See if we have cached a DS record which validates this key */
if (!(crecp = cache_find_by_name(NULL, name, now, F_DS)))
{
strcpy(keyname, name);
return STAT_NEED_DS;
}
-
+
/* NOTE, we need to find ONE DNSKEY which matches the DS */
- for (valid = 0, j = ntohs(header->ancount); j != 0 && !valid; j--)
+ for (j = ntohs(header->ancount); j != 0; j--)
{
/* Ensure we have type, class TTL and length */
if (!(rc = extract_name(header, plen, &p, name, 0, 10)))
@@ -717,7 +833,7 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch
GETLONG(ttl, p);
GETSHORT(rdlen, p);
- if (!CHECK_LEN(header, p, plen, rdlen) || rdlen < 4)
+ if (!CHECK_LEN(header, p, plen, rdlen))
return STAT_BOGUS; /* bad packet */
if (qclass != class || qtype != T_DNSKEY || rc == 2)
@@ -725,171 +841,233 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch
p += rdlen;
continue;
}
-
+
+ if (rdlen < 5)
+ return STAT_BOGUS; /* min 1 byte key! */
+
psave = p;
GETSHORT(flags, p);
if (*p++ != 3)
- return STAT_BOGUS;
- algo = *p++;
- keytag = dnskey_keytag(algo, flags, p, rdlen - 4);
- key = NULL;
-
- /* key must have zone key flag set */
- if (flags & 0x100)
- key = blockdata_alloc((char*)p, rdlen - 4);
-
- p = psave;
-
- if (!ADD_RDLEN(header, p, plen, rdlen))
{
- if (key)
- blockdata_free(key);
- return STAT_BOGUS; /* bad packet */
+ p = psave + rdlen;
+ continue;
}
+ algo = *p++;
+ keyaddr = p;
+ keytag = dnskey_keytag(algo, flags, keyaddr, rdlen - 4);
+
+ p = psave + rdlen;
- /* No zone key flag or malloc failure */
- if (!key)
+ /* key must have zone key flag set */
+ if (!(flags & 0x100))
continue;
+ failflags &= ~DNSSEC_FAIL_NOZONE;
+
+ /* clear digest cache. */
+ memset(valid_digest, 0, sizeof(valid_digest));
+
for (recp1 = crecp; recp1; recp1 = cache_find_by_name(recp1, name, now, F_DS))
{
void *ctx;
unsigned char *digest, *ds_digest;
const struct nettle_hash *hash;
- int sigcnt, rrcnt;
+ int wire_len;
+
+ if ((recp1->flags & F_NEG) ||
+ recp1->addr.ds.algo != algo ||
+ recp1->addr.ds.keytag != keytag ||
+ recp1->uid != (unsigned int)class)
+ continue;
+
+ if (!(hash = hash_find(ds_digest_name(recp1->addr.ds.digest))))
+ continue;
+
+ failflags &= ~DNSSEC_FAIL_NODSSUP;
+
+ if (recp1->addr.ds.keylen != (int)hash->digest_size ||
+ !(ds_digest = blockdata_retrieve(recp1->addr.ds.keydata, recp1->addr.ds.keylen, NULL)))
+ continue;
- if (recp1->addr.ds.algo == algo &&
- recp1->addr.ds.keytag == keytag &&
- recp1->uid == (unsigned int)class &&
- (hash = hash_find(ds_digest_name(recp1->addr.ds.digest))) &&
- hash_init(hash, &ctx, &digest))
-
+ if (valid_digest[recp1->addr.ds.digest])
+ digest = cached_digest[recp1->addr.ds.digest];
+ else
{
- int wire_len = to_wire(name);
+ /* computing a hash is a unit of crypto work. */
+ if (dec_counter(validate_counter, NULL))
+ return STAT_ABANDONED;
+
+ if (!hash_init(hash, &ctx, &digest))
+ continue;
+
+ wire_len = to_wire(name);
/* Note that digest may be different between DSs, so
- we can't move this outside the loop. */
+ we can't move this outside the loop. We keep
+ copies of each digest we make for this key,
+ so maximum digest work is O(keys x digests_types)
+ rather then O(keys x DSs) */
hash->update(ctx, (unsigned int)wire_len, (unsigned char *)name);
hash->update(ctx, (unsigned int)rdlen, psave);
hash->digest(ctx, hash->digest_size, digest);
from_wire(name);
-
- if (!(recp1->flags & F_NEG) &&
- recp1->addr.ds.keylen == (int)hash->digest_size &&
- (ds_digest = blockdata_retrieve(recp1->addr.key.keydata, recp1->addr.ds.keylen, NULL)) &&
- memcmp(ds_digest, digest, recp1->addr.ds.keylen) == 0 &&
- explore_rrset(header, plen, class, T_DNSKEY, name, keyname, &sigcnt, &rrcnt) &&
- sigcnt != 0 && rrcnt != 0 &&
- validate_rrset(now, header, plen, class, T_DNSKEY, sigcnt, rrcnt, name, keyname,
- NULL, key, rdlen - 4, algo, keytag) == STAT_SECURE)
+
+ if (recp1->addr.ds.digest >= cached_digest_size)
{
- valid = 1;
- break;
+ unsigned char **new;
+
+ /* whine_malloc zeros memory */
+ if ((new = whine_malloc((recp1->addr.ds.digest + 5) * sizeof(unsigned char *))))
+ {
+ if (cached_digest_size != 0)
+ {
+ memcpy(new, cached_digest, cached_digest_size * sizeof(unsigned char *));
+ free(cached_digest);
+ }
+
+ cached_digest_size = recp1->addr.ds.digest + 5;
+ cached_digest = new;
+ }
+ }
+
+ if (recp1->addr.ds.digest < cached_digest_size)
+ {
+ if (!cached_digest[recp1->addr.ds.digest])
+ cached_digest[recp1->addr.ds.digest] = whine_malloc(recp1->addr.ds.keylen);
+
+ if (cached_digest[recp1->addr.ds.digest])
+ {
+ memcpy(cached_digest[recp1->addr.ds.digest], digest, recp1->addr.ds.keylen);
+ valid_digest[recp1->addr.ds.digest] = 1;
+ }
}
}
- }
- blockdata_free(key);
- }
-
- if (valid)
- {
- /* DNSKEY RRset determined to be OK, now cache it. */
- cache_start_insert();
-
- p = skip_questions(header, plen);
-
- for (j = ntohs(header->ancount); j != 0; j--)
- {
- /* Ensure we have type, class TTL and length */
- if (!(rc = extract_name(header, plen, &p, name, 0, 10)))
- return STAT_BOGUS; /* bad packet */
- GETSHORT(qtype, p);
- GETSHORT(qclass, p);
- GETLONG(ttl, p);
- GETSHORT(rdlen, p);
-
- if (!CHECK_LEN(header, p, plen, rdlen))
- return STAT_BOGUS; /* bad packet */
-
- if (qclass == class && rc == 1)
+ if (memcmp(ds_digest, digest, recp1->addr.ds.keylen) == 0)
{
- psave = p;
+ /* Found the key validated by a DS record.
+ Now check the self-sig for the entire key RRset using that key.
+ Note that validate_rrset() will never return STAT_NEED_KEY here,
+ since we supply the key it will use as an argument. */
+ struct blockdata *key;
+
+ if (!(key = blockdata_alloc((char *)keyaddr, rdlen - 4)))
+ break;
+
+ rc = validate_rrset(now, header, plen, class, T_DNSKEY, sigcnt, rrcnt, name, keyname,
+ NULL, key, rdlen - 4, algo, keytag, &sig_ttl, validate_counter);
+
+ blockdata_free(key);
+
+ if (STAT_ISEQUAL(rc, STAT_ABANDONED))
+ return rc;
+
+ /* can't validate KEY RRset with this key, see if there's another that
+ will, which is validated by another DS. */
+ if (!STAT_ISEQUAL(rc, STAT_SECURE))
+ break;
- if (qtype == T_DNSKEY)
+ /* DNSKEY RRset determined to be OK, now cache it. */
+ cache_start_insert();
+
+ p = skip_questions(header, plen);
+
+ for (j = ntohs(header->ancount); j != 0; j--)
{
- if (rdlen < 4)
+ /* Ensure we have type, class TTL and length */
+ if (!(rc = extract_name(header, plen, &p, name, 0, 10)))
return STAT_BOGUS; /* bad packet */
- GETSHORT(flags, p);
- if (*p++ != 3)
- return STAT_BOGUS;
- algo = *p++;
- keytag = dnskey_keytag(algo, flags, p, rdlen - 4);
+ GETSHORT(qtype, p);
+ GETSHORT(qclass, p);
+ GETLONG(ttl, p);
+ GETSHORT(rdlen, p);
- /* Cache needs to known class for DNSSEC stuff */
- a.addr.dnssec.class = class;
+ /* TTL may be limited by sig. */
+ if (sig_ttl < ttl)
+ ttl = sig_ttl;
- if ((key = blockdata_alloc((char*)p, rdlen - 4)))
- {
- if (!(recp1 = cache_insert(name, &a, now, ttl, F_FORWARD | F_DNSKEY | F_DNSSECOK)))
- {
- blockdata_free(key);
- return STAT_BOGUS;
- }
- else
- {
- a.addr.log.keytag = keytag;
- a.addr.log.algo = algo;
- if (algo_digest_name(algo))
- log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DNSKEY keytag %hu, algo %hu");
- else
- log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DNSKEY keytag %hu, algo %hu (not supported)");
-
- recp1->addr.key.keylen = rdlen - 4;
- recp1->addr.key.keydata = key;
- recp1->addr.key.algo = algo;
- recp1->addr.key.keytag = keytag;
- recp1->addr.key.flags = flags;
- }
- }
+ if (!CHECK_LEN(header, p, plen, rdlen))
+ return STAT_BOGUS; /* bad packet */
+
+ psave = p;
+
+ if (qclass == class && rc == 1 && qtype == T_DNSKEY)
+ {
+ if (rdlen < 4)
+ return STAT_BOGUS; /* min 1 byte key! */
+
+ GETSHORT(flags, p);
+ if (*p++ == 3)
+ {
+ algo = *p++;
+ keytag = dnskey_keytag(algo, flags, p, rdlen - 4);
+
+ /* Cache needs to known class for DNSSEC stuff */
+ a.addr.dnssec.class = class;
+
+ if (!(key = blockdata_alloc((char*)p, rdlen - 4)))
+ return STAT_BOGUS;
+
+ if (!(recp1 = cache_insert(name, &a, now, ttl, F_FORWARD | F_DNSKEY | F_DNSSECOK)))
+ {
+ blockdata_free(key);
+ return STAT_BOGUS;
+ }
+
+ a.addr.log.keytag = keytag;
+ a.addr.log.algo = algo;
+ if (algo_digest_name(algo))
+ log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DNSKEY keytag %hu, algo %hu");
+ else
+ log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DNSKEY keytag %hu, algo %hu (not supported)");
+
+ recp1->addr.key.keylen = rdlen - 4;
+ recp1->addr.key.keydata = key;
+ recp1->addr.key.algo = algo;
+ recp1->addr.key.keytag = keytag;
+ recp1->addr.key.flags = flags;
+ }
+ }
+
+ p = psave + rdlen;
}
-
- p = psave;
+
+ /* commit cache insert. */
+ cache_end_insert();
+ return STAT_OK;
}
-
- if (!ADD_RDLEN(header, p, plen, rdlen))
- return STAT_BOGUS; /* bad packet */
}
-
- /* commit cache insert. */
- cache_end_insert();
- return STAT_OK;
}
-
+
log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, "BOGUS DNSKEY");
- return STAT_BOGUS;
+ return STAT_BOGUS | failflags;
}
/* The DNS packet is expected to contain the answer to a DS query
- Put all DSs in the answer which are valid into the cache.
+ Put all DSs in the answer which are valid and have hash and signature algos
+ we support into the cache.
Also handles replies which prove that there's no DS at this location,
either because the zone is unsigned or this isn't a zone cut. These are
cached too.
+ If none of the DS's are for supported algos, treat the answer as if
+ it's a proof of no DS at this location. RFC4035 para 5.2.
return codes:
STAT_OK At least one valid DS found and in cache.
STAT_BOGUS no DS in reply or not signed, fails validation, bad packet.
STAT_NEED_KEY DNSKEY records to validate a DS not found, name in keyname
STAT_NEED_DS DS record needed.
+ STAT_ABANDONED resource exhaustion.
*/
-int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int class)
+int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name,
+ char *keyname, int class, int *validate_counter)
{
unsigned char *p = (unsigned char *)(header+1);
- int qtype, qclass, rc, i, neganswer, nons;
- int aclass, atype, rdlen;
+ int qtype, qclass, rc, i, neganswer = 0, nons = 0, servfail = 0, neg_ttl = 0, found_supported = 0;
+ int aclass, atype, rdlen, flags;
unsigned long ttl;
struct all_addr a;
@@ -901,35 +1079,51 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char
GETSHORT(qclass, p);
if (qtype != T_DS || qclass != class)
- rc = STAT_BOGUS;
+ return STAT_BOGUS;
+
+ /* A SERVFAIL answer has been seen to a DS query not at start of authority,
+ so treat it as such and continue to search for a DS or proof of no existence
+ further down the tree. */
+ if (RCODE(header) == SERVFAIL)
+ servfail = neganswer = nons = 1;
else
- rc = dnssec_validate_reply(now, header, plen, name, keyname, NULL, 0, &neganswer, &nons);
+ {
+ rc = dnssec_validate_reply(now, header, plen, name, keyname, NULL, 0, &neganswer, &nons, &neg_ttl, validate_counter);
- if (rc == STAT_INSECURE)
- rc = STAT_BOGUS;
-
- p = (unsigned char *)(header+1);
- extract_name(header, plen, &p, name, 1, 4);
- p += 4; /* qtype, qclass */
+ if (STAT_ISEQUAL(rc, STAT_INSECURE))
+ {
+ my_syslog(LOG_WARNING, _("Insecure DS reply received for %s, check domain configuration and upstream DNS server DNSSEC support"), name);
+ log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, "BOGUS DS - not secure");
+ return STAT_BOGUS | DNSSEC_FAIL_INDET;
+ }
+
+ p = (unsigned char *)(header+1);
+ if (!extract_name(header, plen, &p, name, 1, 4))
+ return STAT_BOGUS;
+
+ p += 4; /* qtype, qclass */
+
+ /* If the key needed to validate the DS is on the same domain as the DS, we'll
+ loop getting nowhere. Stop that now. This can happen of the DS answer comes
+ from the DS's zone, and not the parent zone. */
+ if (STAT_ISEQUAL(rc, STAT_NEED_KEY) && hostname_isequal(name, keyname))
+ {
+ log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, "BOGUS DS");
+ return STAT_BOGUS;
+ }
- /* If the key needed to validate the DS is on the same domain as the DS, we'll
- loop getting nowhere. Stop that now. This can happen of the DS answer comes
- from the DS's zone, and not the parent zone. */
- if (rc == STAT_BOGUS || (rc == STAT_NEED_KEY && hostname_isequal(name, keyname)))
- {
- log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, "BOGUS DS");
- return STAT_BOGUS;
+ if (!STAT_ISEQUAL(rc, STAT_SECURE))
+ return rc;
}
- if (rc != STAT_SECURE)
- return rc;
-
if (!neganswer)
{
cache_start_insert();
for (i = 0; i < ntohs(header->ancount); i++)
{
+ unsigned char *psave;
+
if (!(rc = extract_name(header, plen, &p, name, 0, 10)))
return STAT_BOGUS; /* bad packet */
@@ -940,16 +1134,16 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char
if (!CHECK_LEN(header, p, plen, rdlen))
return STAT_BOGUS; /* bad packet */
+
+ psave = p;
if (aclass == class && atype == T_DS && rc == 1)
{
int algo, digest, keytag;
- unsigned char *psave = p;
struct blockdata *key;
- struct crec *crecp;
-
- if (rdlen < 4)
- return STAT_BOGUS; /* bad packet */
+
+ if (rdlen < 5)
+ return STAT_BOGUS; /* min 1 byte digest! */
GETSHORT(keytag, p);
algo = *p++;
@@ -957,9 +1151,19 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char
/* Cache needs to known class for DNSSEC stuff */
a.addr.dnssec.class = class;
-
- if ((key = blockdata_alloc((char*)p, rdlen - 4)))
+
+ if (!ds_digest_name(digest) || !algo_digest_name(algo))
+ {
+ a.addr.log.keytag = keytag;
+ a.addr.log.algo = algo;
+ a.addr.log.digest = digest;
+ log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DS for keytag %hu, algo %hu, digest %hu (not supported)");
+ neg_ttl = ttl;
+ }
+ else if ((key = blockdata_alloc((char*)p, rdlen - 4)))
{
+ struct crec *crecp;
+
if (!(crecp = cache_insert(name, &a, now, ttl, F_FORWARD | F_DS | F_DNSSECOK)))
{
blockdata_free(key);
@@ -967,39 +1171,36 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char
}
else
{
- a.addr.log.keytag = keytag;
+ a.addr.log.keytag = keytag;
a.addr.log.algo = algo;
a.addr.log.digest = digest;
- if (ds_digest_name(digest) && algo_digest_name(algo))
- log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DS keytag %hu, algo %hu, digest %hu");
- else
- log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DS keytag %hu, algo %hu, digest %hu (not supported)");
-
- crecp->addr.ds.digest = digest;
- crecp->addr.ds.keydata = key;
- crecp->addr.ds.algo = algo;
- crecp->addr.ds.keytag = keytag;
- crecp->addr.ds.keylen = rdlen - 4;
+ log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DS for keytag %hu, algo %hu, digest %hu");
+
+ crecp->addr.ds.digest = digest;
+ crecp->addr.ds.keydata = key;
+ crecp->addr.ds.algo = algo;
+ crecp->addr.ds.keytag = keytag;
+ crecp->addr.ds.keylen = rdlen - 4;
+
+ found_supported = 1;
}
}
-
- p = psave;
}
- if (!ADD_RDLEN(header, p, plen, rdlen))
- return STAT_BOGUS; /* bad packet */
+
+ p = psave + rdlen;
}
cache_end_insert();
+ /* Fall through if no supported algo DS found. */
+ if (found_supported)
+ return STAT_OK;
}
- else
+
+ flags = F_FORWARD | F_DS | F_NEG | F_DNSSECOK;
+
+ if (neganswer)
{
- int flags = F_FORWARD | F_DS | F_NEG | F_DNSSECOK;
- unsigned long minttl = ULONG_MAX;
-
- if (!(p = skip_section(p, ntohs(header->ancount), header, plen)))
- return STAT_BOGUS;
-
if (RCODE(header) == NXDOMAIN)
flags |= F_NXDOMAIN;
@@ -1007,57 +1208,21 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char
to store presence/absence of NS. */
if (nons)
flags &= ~F_DNSSECOK;
-
- for (i = ntohs(header->nscount); i != 0; i--)
- {
- if (!(p = skip_name(p, header, plen, 0)))
- return STAT_BOGUS;
-
- GETSHORT(atype, p);
- GETSHORT(aclass, p);
- GETLONG(ttl, p);
- GETSHORT(rdlen, p);
-
- if (!CHECK_LEN(header, p, plen, rdlen))
- return STAT_BOGUS; /* bad packet */
-
- if (aclass != class || atype != T_SOA)
- {
- p += rdlen;
- continue;
- }
-
- if (ttl < minttl)
- minttl = ttl;
-
- /* MNAME */
- if (!(p = skip_name(p, header, plen, 0)))
- return STAT_BOGUS;
- /* RNAME */
- if (!(p = skip_name(p, header, plen, 20)))
- return STAT_BOGUS;
- p += 16; /* SERIAL REFRESH RETRY EXPIRE */
-
- GETLONG(ttl, p); /* minTTL */
- if (ttl < minttl)
- minttl = ttl;
-
- break;
- }
-
- if (i != 0)
- {
- cache_start_insert();
-
- a.addr.dnssec.class = class;
- if (!cache_insert(name, &a, now, ttl, flags))
- return STAT_BOGUS;
-
- cache_end_insert();
-
- log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, "no DS");
- }
}
+
+ cache_start_insert();
+
+ a.addr.dnssec.class = class;
+
+ /* Use TTL from NSEC for negative cache entries */
+ if (!cache_insert(name, NULL, now, neg_ttl, flags))
+ return STAT_BOGUS;
+
+ cache_end_insert();
+
+ if (neganswer)
+ log_query(F_NOEXTRA | F_UPSTREAM, name, NULL,
+ servfail ? "SERVFAIL" : (nons ? "no DS/cut" : "no DS"));
return STAT_OK;
}
@@ -1127,6 +1292,7 @@ static int hostname_cmp(const char *a, const char *b)
}
}
+/* returns 0 on success, or DNSSEC_FAIL_* value on failure. */
static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsigned char **nsecs, unsigned char **labels, int nsec_count,
char *workspace1_in, char *workspace2, char *name, int type, int *nons)
{
@@ -1146,12 +1312,12 @@ static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsi
p = nsecs[i];
if (!extract_name(header, plen, &p, workspace1, 1, 10))
- return 0;
+ return DNSSEC_FAIL_BADPACKET;
p += 8; /* class, type, TTL */
GETSHORT(rdlen, p);
psave = p;
- if (!extract_name(header, plen, &p, workspace2, 1, 10))
- return 0;
+ if (!extract_name(header, plen, &p, workspace2, 1, 0))
+ return DNSSEC_FAIL_BADPACKET;
/* If NSEC comes from wildcard expansion, use original wildcard
as name for computation. */
@@ -1179,12 +1345,13 @@ static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsi
{
/* 4035 para 5.4. Last sentence */
if (type == T_NSEC || type == T_RRSIG)
- return 1;
+ return 0;
/* NSEC with the same name as the RR we're testing, check
that the type in question doesn't appear in the type map */
rdlen -= p - psave;
- /* rdlen is now length of type map, and p points to it */
+ /* rdlen is now length of type map, and p points to it
+ packet checked to be as long as rdlen implies in prove_non_existence() */
/* If we can prove that there's no NS record, return that information. */
if (nons && rdlen >= 2 && p[0] == 0 && (p[2] & (0x80 >> T_NS)) != 0)
@@ -1195,25 +1362,25 @@ static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsi
/* A CNAME answer would also be valid, so if there's a CNAME is should
have been returned. */
if ((p[2] & (0x80 >> T_CNAME)) != 0)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
/* If the SOA bit is set for a DS record, then we have the
DS from the wrong side of the delegation. For the root DS,
this is expected. */
if (name_labels != 0 && type == T_DS && (p[2] & (0x80 >> T_SOA)) != 0)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
}
while (rdlen >= 2)
{
if (!CHECK_LEN(header, p, plen, rdlen))
- return 0;
+ return DNSSEC_FAIL_BADPACKET;
if (p[0] == type >> 8)
{
/* Does the NSEC say our type exists? */
if (offset < p[1] && (p[offset+2] & mask) != 0)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
break; /* finished checking */
}
@@ -1222,24 +1389,24 @@ static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsi
p += p[1];
}
- return 1;
+ return 0;
}
else if (rc == -1)
{
/* Normal case, name falls between NSEC name and next domain name,
wrap around case, name falls between NSEC name (rc == -1) and end */
if (hostname_cmp(workspace2, name) >= 0 || hostname_cmp(workspace1, workspace2) >= 0)
- return 1;
+ return 0;
}
else
{
/* wrap around case, name falls between start and next domain name */
if (hostname_cmp(workspace1, workspace2) >= 0 && hostname_cmp(workspace2, name) >=0 )
- return 1;
+ return 0;
}
}
- return 0;
+ return DNSSEC_FAIL_NONSEC;
}
/* return digest length, or zero on error */
@@ -1313,23 +1480,23 @@ static int check_nsec3_coverage(struct dns_header *header, size_t plen, int dige
for (i = 0; i < nsec_count; i++)
if ((p = nsecs[i]))
{
- if (!extract_name(header, plen, &p, workspace1, 1, 0) ||
+ if (!extract_name(header, plen, &p, workspace1, 1, 10) ||
!(base32_len = base32_decode(workspace1, (unsigned char *)workspace2)))
return 0;
p += 8; /* class, type, TTL */
GETSHORT(rdlen, p);
+
psave = p;
+
+ /* packet checked to be as long as implied by rdlen, salt_len and hash_len in prove_non_existence() */
p++; /* algo */
flags = *p++; /* flags */
p += 2; /* iterations */
salt_len = *p++; /* salt_len */
p += salt_len; /* salt */
hash_len = *p++; /* p now points to next hashed name */
-
- if (!CHECK_LEN(header, p, plen, hash_len))
- return 0;
-
+
if (digest_len == base32_len && hash_len == base32_len)
{
int rc = memcmp(workspace2, digest, digest_len);
@@ -1337,7 +1504,8 @@ static int check_nsec3_coverage(struct dns_header *header, size_t plen, int dige
if (rc == 0)
{
/* We found an NSEC3 whose hashed name exactly matches the query, so
- we just need to check the type map. p points to the RR data for the record. */
+ we just need to check the type map. p points to the RR data for the record.
+ Note we have packet length up to rdlen bytes checked. */
int offset = (type & 0xff) >> 3;
int mask = 0x80 >> (type & 0x07);
@@ -1345,15 +1513,12 @@ static int check_nsec3_coverage(struct dns_header *header, size_t plen, int dige
p += hash_len; /* skip next-domain hash */
rdlen -= p - psave;
- if (!CHECK_LEN(header, p, plen, rdlen))
- return 0;
-
if (rdlen >= 2 && p[0] == 0)
{
/* If we can prove that there's no NS record, return that information. */
if (nons && (p[2] & (0x80 >> T_NS)) != 0)
*nons = 0;
-
+
/* A CNAME answer would also be valid, so if there's a CNAME is should
have been returned. */
if ((p[2] & (0x80 >> T_CNAME)) != 0)
@@ -1412,8 +1577,9 @@ static int check_nsec3_coverage(struct dns_header *header, size_t plen, int dige
return 0;
}
-static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, unsigned char **nsecs, int nsec_count,
- char *workspace1, char *workspace2, char *name, int type, char *wildname, int *nons)
+/* returns 0 on success, or DNSSEC_FAIL_* value on failure. */
+static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, unsigned char **nsecs, int nsec_count, char *workspace1,
+ char *workspace2, char *name, int type, char *wildname, int *nons, int *validate_counter)
{
unsigned char *salt, *p, *digest;
int digest_len, i, iterations, salt_len, base32_len, algo = 0;
@@ -1433,7 +1599,7 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
for (i = 0; i < nsec_count; i++)
{
if (!(p = skip_name(nsecs[i], header, plen, 15)))
- return 0; /* bad packet */
+ return DNSSEC_FAIL_BADPACKET; /* bad packet */
p += 10; /* type, class, TTL, rdlen */
algo = *p++;
@@ -1444,23 +1610,18 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
/* No usable NSEC3s */
if (i == nsec_count)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
p++; /* flags */
GETSHORT (iterations, p);
- /* Upper-bound iterations, to avoid DoS.
- Strictly, there are lower bounds for small keys, but
- since we don't have key size info here, at least limit
- to the largest bound, for 4096-bit keys. RFC 5155 10.3 */
- if (iterations > 2500)
- return 0;
+ /* Upper-bound iterations, to avoid DoS. RFC 9276 refers. */
+ if (iterations > daemon->limit[LIMIT_NSEC3_ITERS])
+ return DNSSEC_FAIL_NSEC3_ITERS;
salt_len = *p++;
salt = p;
- if (!CHECK_LEN(header, salt, plen, salt_len))
- return 0; /* bad packet */
-
+
/* Now prune so we only have NSEC3 records with same iterations, salt and algo */
for (i = 0; i < nsec_count; i++)
{
@@ -1470,7 +1631,7 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
nsecs[i] = NULL; /* Speculative, will be restored if OK. */
if (!(p = skip_name(nsec3p, header, plen, 15)))
- return 0; /* bad packet */
+ return DNSSEC_FAIL_BADPACKET; /* bad packet */
p += 10; /* type, class, TTL, rdlen */
@@ -1490,9 +1651,6 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
if (salt_len != *p++)
continue;
- if (!CHECK_LEN(header, p, plen, salt_len))
- return 0; /* bad packet */
-
if (memcmp(p, salt, salt_len) != 0)
continue;
@@ -1500,11 +1658,14 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
nsecs[i] = nsec3p;
}
+ if (dec_counter(validate_counter, NULL))
+ return DNSSEC_FAIL_WORK;
+
if ((digest_len = hash_name(name, &digest, hash, salt, salt_len, iterations)) == 0)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
if (check_nsec3_coverage(header, plen, digest_len, digest, type, workspace1, workspace2, nsecs, nsec_count, nons, count_labels(name)))
- return 1;
+ return 0;
/* Can't find an NSEC3 which covers the name directly, we need the "closest encloser NSEC3"
or an answer inferred from a wildcard record. */
@@ -1519,15 +1680,20 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
if (wildname && hostname_isequal(closest_encloser, wildname))
break;
+ if (dec_counter(validate_counter, NULL))
+ return DNSSEC_FAIL_WORK;
+
if ((digest_len = hash_name(closest_encloser, &digest, hash, salt, salt_len, iterations)) == 0)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
for (i = 0; i < nsec_count; i++)
if ((p = nsecs[i]))
{
- if (!extract_name(header, plen, &p, workspace1, 1, 0) ||
- !(base32_len = base32_decode(workspace1, (unsigned char *)workspace2)))
- return 0;
+ if (!extract_name(header, plen, &p, workspace1, 1, 0))
+ return DNSSEC_FAIL_BADPACKET;
+
+ if (!(base32_len = base32_decode(workspace1, (unsigned char *)workspace2)))
+ return DNSSEC_FAIL_NONSEC;
if (digest_len == base32_len &&
memcmp(digest, workspace2, digest_len) == 0)
@@ -1542,35 +1708,43 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
while ((closest_encloser = strchr(closest_encloser, '.')));
if (!closest_encloser || !next_closest)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
/* Look for NSEC3 that proves the non-existence of the next-closest encloser */
+ if (dec_counter(validate_counter, NULL))
+ return DNSSEC_FAIL_WORK;
+
if ((digest_len = hash_name(next_closest, &digest, hash, salt, salt_len, iterations)) == 0)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
if (!check_nsec3_coverage(header, plen, digest_len, digest, type, workspace1, workspace2, nsecs, nsec_count, NULL, 1))
- return 0;
+ return DNSSEC_FAIL_NONSEC;
/* Finally, check that there's no seat of wildcard synthesis */
if (!wildname)
{
if (!(wildcard = strchr(next_closest, '.')) || wildcard == next_closest)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
wildcard--;
*wildcard = '*';
+ if (dec_counter(validate_counter, NULL))
+ return DNSSEC_FAIL_WORK;
+
if ((digest_len = hash_name(wildcard, &digest, hash, salt, salt_len, iterations)) == 0)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
if (!check_nsec3_coverage(header, plen, digest_len, digest, type, workspace1, workspace2, nsecs, nsec_count, NULL, 1))
- return 0;
+ return DNSSEC_FAIL_NONSEC;
}
- return 1;
+ return 0;
}
-static int prove_non_existence(struct dns_header *header, size_t plen, char *keyname, char *name, int qtype, int qclass, char *wildname, int *nons)
+/* returns 0 on success, or DNSSEC_FAIL_* value on failure. */
+static int prove_non_existence(struct dns_header *header, size_t plen, char *keyname, char *name, int qtype, int qclass,
+ char *wildname, int *nons, int *nsec_ttl, int *validate_counter)
{
static unsigned char **nsecset = NULL, **rrsig_labels = NULL;
static int nsecset_sz = 0, rrsig_labels_sz = 0;
@@ -1578,35 +1752,47 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
int type_found = 0;
unsigned char *auth_start, *p = skip_questions(header, plen);
int type, class, rdlen, i, nsecs_found;
+ unsigned long ttl;
/* Move to NS section */
if (!p || !(p = skip_section(p, ntohs(header->ancount), header, plen)))
- return 0;
+ return DNSSEC_FAIL_BADPACKET;
auth_start = p;
- for (nsecs_found = 0, i = ntohs(header->nscount); i != 0; i--)
+ for (nsecs_found = 0, i = 0; i < ntohs(header->nscount); i++)
{
unsigned char *pstart = p;
if (!extract_name(header, plen, &p, daemon->workspacename, 1, 10))
- return 0;
+ return DNSSEC_FAIL_BADPACKET;
GETSHORT(type, p);
GETSHORT(class, p);
- p += 4; /* TTL */
+ GETLONG(ttl, p);
GETSHORT(rdlen, p);
-
+
+ if (!CHECK_LEN(header, p, plen, rdlen))
+ return DNSSEC_FAIL_BADPACKET;
+
if (class == qclass && (type == T_NSEC || type == T_NSEC3))
{
+ if (nsec_ttl)
+ {
+ /* Limit TTL with sig TTL */
+ if (daemon->rr_status[ntohs(header->ancount) + i] < ttl)
+ ttl = daemon->rr_status[ntohs(header->ancount) + i];
+ *nsec_ttl = ttl;
+ }
+
/* No mixed NSECing 'round here, thankyouverymuch */
if (type_found != 0 && type_found != type)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
type_found = type;
if (!expand_workspace(&nsecset, &nsecset_sz, nsecs_found))
- return 0;
+ return DNSSEC_FAIL_BADPACKET;
if (type == T_NSEC)
{
@@ -1621,30 +1807,33 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
int res, j, rdlen1, type1, class1;
if (!expand_workspace(&rrsig_labels, &rrsig_labels_sz, nsecs_found))
- return 0;
+ return DNSSEC_FAIL_BADPACKET;
rrsig_labels[nsecs_found] = NULL;
for (j = ntohs(header->nscount); j != 0; j--)
{
- if (!(res = extract_name(header, plen, &p1, daemon->workspacename, 0, 10)))
- return 0;
+ unsigned char *psav;
+ if (!(res = extract_name(header, plen, &p1, daemon->workspacename, 0, 10)))
+ return DNSSEC_FAIL_BADPACKET;
+
GETSHORT(type1, p1);
GETSHORT(class1, p1);
p1 += 4; /* TTL */
GETSHORT(rdlen1, p1);
+ psav = p1;
+
if (!CHECK_LEN(header, p1, plen, rdlen1))
- return 0;
+ return DNSSEC_FAIL_BADPACKET;
if (res == 1 && class1 == qclass && type1 == T_RRSIG)
{
int type_covered;
- unsigned char *psav = p1;
-
+
if (rdlen1 < 18)
- return 0; /* bad packet */
+ return DNSSEC_FAIL_BADPACKET; /* bad packet */
GETSHORT(type_covered, p1);
@@ -1656,33 +1845,56 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
if (!rrsig_labels[nsecs_found])
rrsig_labels[nsecs_found] = p1;
else if (*rrsig_labels[nsecs_found] != *p1) /* algo */
- return 0;
- }
- p1 = psav;
+ return DNSSEC_FAIL_NONSEC;
+ }
}
- if (!ADD_RDLEN(header, p1, plen, rdlen1))
- return 0;
+ p1 = psav + rdlen1;
}
/* Must have found at least one sig. */
if (!rrsig_labels[nsecs_found])
- return 0;
+ return DNSSEC_FAIL_NONSEC;
+ }
+ else if (type == T_NSEC3)
+ {
+ /* Decode the packet structure enough to check that rdlen is big enough
+ to contain everything other than the type bitmap.
+ (packet checked to be long enough to contain rdlen above)
+ We don't need to do any further length checks in check_nes3_coverage()
+ or prove_non_existence_nsec3() */
+
+ int salt_len, hash_len;
+ unsigned char *psav = p;
+
+ if (rdlen < 5)
+ return DNSSEC_FAIL_BADPACKET;
+
+ p += 4; /* algo, flags, iterations */
+ salt_len = *p++; /* salt_len */
+ if (rdlen < (6 + salt_len))
+ return DNSSEC_FAIL_BADPACKET; /* check up to hash_length */
+
+ p += salt_len; /* salt */
+ hash_len = *p++;
+ if (rdlen < (6 + salt_len + hash_len))
+ return DNSSEC_FAIL_BADPACKET; /* check to end of next hashed name */
+
+ p = psav;
}
nsecset[nsecs_found++] = pstart;
}
- if (!ADD_RDLEN(header, p, plen, rdlen))
- return 0;
+ p += rdlen;
}
if (type_found == T_NSEC)
return prove_non_existence_nsec(header, plen, nsecset, rrsig_labels, nsecs_found, daemon->workspacename, keyname, name, qtype, nons);
else if (type_found == T_NSEC3)
- return prove_non_existence_nsec3(header, plen, nsecset, nsecs_found, daemon->workspacename, keyname, name, qtype, wildname, nons);
+ return prove_non_existence_nsec3(header, plen, nsecset, nsecs_found, daemon->workspacename, keyname, name, qtype, wildname, nons, validate_counter);
else
- return 0;
+ return DNSSEC_FAIL_NONSEC;
}
/* Check signing status of name.
@@ -1777,35 +1989,40 @@ static int zone_status(char *name, int class, char *keyname, time_t now)
STAT_BOGUS signature is wrong, bad packet, no validation where there should be.
STAT_NEED_KEY need DNSKEY to complete validation (name is returned in keyname, class in *class)
STAT_NEED_DS need DS to complete validation (name is returned in keyname)
+ STAT_ABANDONED resource exhaustion.
daemon->rr_status points to a char array which corressponds to the RRs in the
- answer section (only). This is set to 1 for each RR which is validated, and 0 for any which aren't.
+ answer and auth sections. This is set to >1 for each RR which is validated, and 0 for any which aren't.
+
+ When validating replies to DS records, we're only interested in the NSEC{3} RRs in the auth section.
+ Other RRs in that section missing sigs will not cause am INSECURE reply. We determine this mode
+ if the nons argument is non-NULL.
*/
int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname,
- int *class, int check_unsigned, int *neganswer, int *nons)
+ int *class, int check_unsigned, int *neganswer, int *nons, int *nsec_ttl, int *validate_counter)
{
static unsigned char **targets = NULL;
static int target_sz = 0;
unsigned char *ans_start, *p1, *p2;
int type1, class1, rdlen1 = 0, type2, class2, rdlen2, qclass, qtype, targetidx;
- int i, j, rc;
+ int i, j, rc = STAT_INSECURE;
int secure = STAT_SECURE;
-
+ int rc_nsec;
/* extend rr_status if necessary */
- if (daemon->rr_status_sz < ntohs(header->ancount))
+ if (daemon->rr_status_sz < ntohs(header->ancount) + ntohs(header->nscount))
{
- char *new = whine_malloc(ntohs(header->ancount) + 64);
+ unsigned long *new = whine_malloc(sizeof(*daemon->rr_status) * (ntohs(header->ancount) + ntohs(header->nscount) + 64));
if (!new)
return STAT_BOGUS;
free(daemon->rr_status);
- daemon->rr_status = new;
- daemon->rr_status_sz = ntohs(header->ancount) + 64;
+ daemon->rr_status = (char *)new;
+ daemon->rr_status_sz = ntohs(header->ancount) + ntohs(header->nscount) + 64;
}
- memset(daemon->rr_status, 0, ntohs(header->ancount));
+ memset(daemon->rr_status, 0, sizeof(*daemon->rr_status) * daemon->rr_status_sz);
if (neganswer)
*neganswer = 0;
@@ -1820,7 +2037,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
/* Find all the targets we're looking for answers to.
The zeroth array element is for the query, subsequent ones
- for CNAME targets, unless the query is for a CNAME. */
+ for CNAME targets, unless the query is for a CNAME or ANY. */
if (!expand_workspace(&targets, &target_sz, 0))
return STAT_BOGUS;
@@ -1839,7 +2056,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
if (qtype == T_RRSIG)
return STAT_INSECURE;
- if (qtype != T_CNAME)
+ if (qtype != T_CNAME && qtype != T_ANY)
for (j = ntohs(header->ancount); j != 0; j--)
{
if (!(p1 = skip_name(p1, header, plen, 10)))
@@ -1863,10 +2080,10 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
for (p1 = ans_start, i = 0; i < ntohs(header->ancount) + ntohs(header->nscount); i++)
{
- if (i != 0 && !ADD_RDLEN(header, p1, plen, rdlen1))
- return STAT_BOGUS;
-
- if (!extract_name(header, plen, &p1, name, 1, 10))
+ if (i != 0 && !ADD_RDLEN(header, p1, plen, rdlen1))
+ return STAT_BOGUS;
+
+ if (!extract_name(header, plen, &p1, name, 1, 10))
return STAT_BOGUS; /* bad packet */
GETSHORT(type1, p1);
@@ -1896,12 +2113,9 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
return STAT_BOGUS;
}
+ /* Done already: copy the validation status */
if (j != i)
- {
- /* Done already: copy the validation status */
- if (i < ntohs(header->ancount))
- daemon->rr_status[i] = daemon->rr_status[j];
- }
+ daemon->rr_status[i] = daemon->rr_status[j];
else
{
/* Not done, validate now */
@@ -1914,19 +2128,32 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
/* No signatures for RRset. We can be configured to assume this is OK and return an INSECURE result. */
if (sigcnt == 0)
{
- if (check_unsigned)
+ /* NSEC and NSEC3 records must be signed. We make this assumption elsewhere. */
+ if (type1 == T_NSEC || type1 == T_NSEC3)
+ return STAT_BOGUS | DNSSEC_FAIL_NOSIG;
+ else if (nons && i >= ntohs(header->ancount))
+ /* If we're validating a DS reply, rather than looking for the value of AD bit,
+ we only care that NSEC and NSEC3 RRs in the auth section are signed.
+ Return SECURE even if others (SOA....) are not. */
+ rc = STAT_SECURE;
+ else
{
- rc = zone_status(name, class1, keyname, now);
- if (rc == STAT_SECURE)
- rc = STAT_BOGUS;
- if (class)
- *class = class1; /* Class for NEED_DS or NEED_KEY */
+ /* unsigned RRsets in auth section are not BOGUS, but do make reply insecure. */
+ if (check_unsigned && i < ntohs(header->ancount))
+ {
+ rc = zone_status(name, class1, keyname, now);
+ if (STAT_ISEQUAL(rc, STAT_SECURE))
+ rc = STAT_BOGUS | DNSSEC_FAIL_NOSIG;
+
+ if (class)
+ *class = class1; /* Class for NEED_DS or NEED_KEY */
+ }
+ else
+ rc = STAT_INSECURE;
+
+ if (!STAT_ISEQUAL(rc, STAT_INSECURE))
+ return rc;
}
- else
- rc = STAT_INSECURE;
-
- if (rc != STAT_INSECURE)
- return rc;
}
else
{
@@ -1935,21 +2162,21 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
strcpy(daemon->workspacename, keyname);
rc = zone_status(daemon->workspacename, class1, keyname, now);
- if (rc == STAT_BOGUS || rc == STAT_NEED_KEY || rc == STAT_NEED_DS)
+ if (STAT_ISEQUAL(rc, STAT_BOGUS) || STAT_ISEQUAL(rc, STAT_NEED_KEY) || STAT_ISEQUAL(rc, STAT_NEED_DS))
{
- /* Zone is insecure, don't need to validate RRset */
if (class)
*class = class1; /* Class for NEED_DS or NEED_KEY */
return rc;
}
/* Zone is insecure, don't need to validate RRset */
- if (rc == STAT_SECURE)
+ if (STAT_ISEQUAL(rc, STAT_SECURE))
{
+ unsigned long sig_ttl;
rc = validate_rrset(now, header, plen, class1, type1, sigcnt,
- rrcnt, name, keyname, &wildname, NULL, 0, 0, 0);
+ rrcnt, name, keyname, &wildname, NULL, 0, 0, 0, &sig_ttl, validate_counter);
- if (rc == STAT_BOGUS || rc == STAT_NEED_KEY || rc == STAT_NEED_DS)
+ if (STAT_ISEQUAL(rc, STAT_BOGUS) || STAT_ISEQUAL(rc, STAT_NEED_KEY) || STAT_ISEQUAL(rc, STAT_NEED_DS) || STAT_ISEQUAL(rc, STAT_ABANDONED))
{
if (class)
*class = class1; /* Class for DS or DNSKEY */
@@ -1959,8 +2186,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
/* rc is now STAT_SECURE or STAT_SECURE_WILDCARD */
/* Note that RR is validated */
- if (i < ntohs(header->ancount))
- daemon->rr_status[i] = 1;
+ daemon->rr_status[i] = sig_ttl;
/* Note if we've validated either the answer to the question
or the target of a CNAME. Any not noted will need NSEC or
@@ -1983,16 +2209,16 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
Note that we may not yet have validated the NSEC/NSEC3 RRsets.
That's not a problem since if the RRsets later fail
we'll return BOGUS then. */
- if (rc == STAT_SECURE_WILDCARD &&
- !prove_non_existence(header, plen, keyname, name, type1, class1, wildname, NULL))
- return STAT_BOGUS;
+ if (STAT_ISEQUAL(rc, STAT_SECURE_WILDCARD) &&
+ ((rc_nsec = prove_non_existence(header, plen, keyname, name, type1, class1, wildname, NULL, NULL, validate_counter))) != 0)
+ return (rc_nsec & DNSSEC_FAIL_WORK) ? STAT_ABANDONED : (STAT_BOGUS | rc_nsec);
rc = STAT_SECURE;
}
}
}
- if (rc == STAT_INSECURE)
+ if (STAT_ISEQUAL(rc, STAT_INSECURE))
secure = STAT_INSECURE;
}
@@ -2002,28 +2228,32 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
{
if (neganswer)
*neganswer = 1;
-
+
if (!extract_name(header, plen, &p2, name, 1, 10))
return STAT_BOGUS; /* bad packet */
-
+
/* NXDOMAIN or NODATA reply, unanswered question is (name, qclass, qtype) */
-
+
/* For anything other than a DS record, this situation is OK if either
the answer is in an unsigned zone, or there's a NSEC records. */
- if (!prove_non_existence(header, plen, keyname, name, qtype, qclass, NULL, nons))
+ if ((rc_nsec = prove_non_existence(header, plen, keyname, name, qtype, qclass, NULL, nons, nsec_ttl, validate_counter)) != 0)
{
+ if (rc_nsec & DNSSEC_FAIL_WORK)
+ return STAT_ABANDONED;
+
/* Empty DS without NSECS */
if (qtype == T_DS)
- return STAT_BOGUS;
+ return STAT_BOGUS | rc_nsec;
- if ((rc = zone_status(name, qclass, keyname, now)) != STAT_SECURE)
+ if ((rc_nsec & (DNSSEC_FAIL_NONSEC | DNSSEC_FAIL_NSEC3_ITERS)) &&
+ !STAT_ISEQUAL((rc = zone_status(name, qclass, keyname, now)), STAT_SECURE))
{
if (class)
*class = qclass; /* Class for NEED_DS or NEED_KEY */
return rc;
}
- return STAT_BOGUS; /* signed zone, no NSECs */
+ return STAT_BOGUS | rc_nsec; /* signed zone, no NSECs */
}
}
diff --git a/src/forward.c b/src/forward.c
index 989fb61..64124b1 100644
--- a/src/forward.c
+++ b/src/forward.c
@@ -406,7 +406,8 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr,
if (ad_reqd)
forward->flags |= FREC_AD_QUESTION;
#ifdef HAVE_DNSSEC
- forward->work_counter = DNSSEC_WORK;
+ forward->work_counter = daemon->limit[LIMIT_WORK];
+ forward->validate_counter = daemon->limit[LIMIT_CRYPTO];
if (do_bit)
forward->flags |= FREC_DO_QUESTION;
#endif
@@ -770,6 +771,7 @@ void reply_query(int fd, time_t now)
struct server *server;
void *hash;
struct server_domain *sd;
+ size_t plen;
/* packet buffer overwritten */
daemon->srv_save = NULL;
@@ -820,7 +822,6 @@ void reply_query(int fd, time_t now)
/* for broken servers, attempt to send to another one. */
{
unsigned char *pheader;
- size_t plen;
int is_sign;
/* In strict order mode, there must be a server later in the chain
@@ -926,29 +927,38 @@ void reply_query(int fd, time_t now)
*/
if (header->hb3 & HB3_TC)
status = STAT_TRUNCATED;
+ struct frec *orig;
+ int log_resource = 0;
while (1)
{
/* As soon as anything returns BOGUS, we stop and unwind, to do otherwise
would invite infinite loops, since the answers to DNSKEY and DS queries
will not be cached, so they'll be repeated. */
+ /* Find the original query that started it all.... */
+ for (orig = forward; orig->dependent; orig = orig->dependent);
+
if (status != STAT_BOGUS && status != STAT_TRUNCATED && status != STAT_ABANDONED)
{
if (forward->flags & FREC_DNSKEY_QUERY)
- status = dnssec_validate_by_ds(now, header, n, daemon->namebuff, daemon->keyname, forward->class);
+ status = dnssec_validate_by_ds(now, header, n, daemon->namebuff, daemon->keyname, forward->class, &orig->validate_counter);
else if (forward->flags & FREC_DS_QUERY)
- status = dnssec_validate_ds(now, header, n, daemon->namebuff, daemon->keyname, forward->class);
+ status = dnssec_validate_ds(now, header, n, daemon->namebuff, daemon->keyname, forward->class, &orig->validate_counter);
else
status = dnssec_validate_reply(now, header, n, daemon->namebuff, daemon->keyname, &forward->class,
option_bool(OPT_DNSSEC_NO_SIGN) && (server->flags & SERV_DO_DNSSEC),
- NULL, NULL);
+ NULL, NULL, NULL, &orig->validate_counter);
}
+ status = status & 0xffff0000;
+
+ if (status == STAT_ABANDONED)
+ log_resource = 1;
/* Can't validate, as we're missing key data. Put this
answer aside, whilst we get that. */
if (status == STAT_NEED_DS || status == STAT_NEED_KEY)
{
- struct frec *new, *orig;
+ struct frec *new;
/* Free any saved query */
if (forward->stash)
@@ -959,11 +969,11 @@ void reply_query(int fd, time_t now)
return;
forward->stash_len = n;
- /* Find the original query that started it all.... */
- for (orig = forward; orig->dependent; orig = orig->dependent);
-
- if (--orig->work_counter == 0 || !(new = get_new_frec(now, NULL, 1)))
+ if (--orig->work_counter == 0 || !(new = get_new_frec(now, NULL, 1))) {
status = STAT_ABANDONED;
+ my_syslog(LOG_WARNING, _("limit exceeded: per-query subqueries"));
+ log_resource = 1;
+ }
else
{
int fd, type = SERV_DO_DNSSEC;
@@ -1056,7 +1066,15 @@ void reply_query(int fd, time_t now)
}
return;
}
-
+ if (log_resource)
+ {
+ /* Log the actual validation that made us barf. */
+ unsigned char *p = (unsigned char *)(header+1);
+ if (extract_name(header, plen, &p, daemon->namebuff, 0, 4) == 1)
+ my_syslog(LOG_WARNING, _("validation of %s failed: resource limit exceeded."),
+ daemon->namebuff[0] ? daemon->namebuff : ".");
+ }
+
/* Validated original answer, all done. */
if (!forward->dependent)
break;
@@ -1499,7 +1517,7 @@ void receive_query(struct listener *listen, time_t now)
/* Recurse up the key hierarchy */
static int tcp_key_recurse(time_t now, int status, struct dns_header *header, size_t n,
int class, char *name, char *keyname, struct server *server,
- int have_mark, unsigned int mark, int *keycount)
+ int have_mark, unsigned int mark, int *keycount, int *validatecount)
{
int new_status;
unsigned char *packet = NULL;
@@ -1516,20 +1534,35 @@ static int tcp_key_recurse(time_t now, int status, struct dns_header *header, si
struct server *firstsendto = NULL;
/* limit the amount of work we do, to avoid cycling forever on loops in the DNS */
- if (--(*keycount) == 0)
- new_status = STAT_ABANDONED;
- else if (status == STAT_NEED_KEY)
- new_status = dnssec_validate_by_ds(now, header, n, name, keyname, class);
+ if (status == STAT_NEED_KEY)
+ new_status = dnssec_validate_by_ds(now, header, n, name, keyname, class, validatecount);
else if (status == STAT_NEED_DS)
- new_status = dnssec_validate_ds(now, header, n, name, keyname, class);
+ new_status = dnssec_validate_ds(now, header, n, name, keyname, class, validatecount);
else
new_status = dnssec_validate_reply(now, header, n, name, keyname, &class,
- option_bool(OPT_DNSSEC_NO_SIGN) && (server->flags & SERV_DO_DNSSEC),
- NULL, NULL);
-
- if (new_status != STAT_NEED_DS && new_status != STAT_NEED_KEY)
+ !option_bool(OPT_DNSSEC_NO_SIGN) && (server->flags & SERV_DO_DNSSEC),
+ NULL, NULL, NULL, validatecount);
+ new_status = new_status & 0xffff0000;
+
+ if (new_status != STAT_NEED_DS && new_status != STAT_NEED_KEY && new_status != STAT_ABANDONED)
break;
+ if ((*keycount)-- == 0)
+ {
+ my_syslog(LOG_WARNING, _("limit exceeded: per-query subqueries"));
+ new_status = STAT_ABANDONED;
+ }
+
+ if (new_status == STAT_ABANDONED)
+ {
+ /* Log the actual validation that made us barf. */
+ unsigned char *p = (unsigned char *)(header+1);
+ if (extract_name(header, n, &p, daemon->namebuff, 0, 4) == 1)
+ my_syslog(LOG_WARNING, _("validation of %s failed: resource limit exceeded."),
+ daemon->namebuff[0] ? daemon->namebuff : ".");
+ break;
+ }
+
/* Can't validate because we need a key/DS whose name now in keyname.
Make query for same, and recurse to validate */
if (!packet)
@@ -1624,7 +1657,7 @@ static int tcp_key_recurse(time_t now, int status, struct dns_header *header, si
server->flags |= SERV_GOT_TCP;
m = (c1 << 8) | c2;
- new_status = tcp_key_recurse(now, new_status, new_header, m, class, name, keyname, server, have_mark, mark, keycount);
+ new_status = tcp_key_recurse(now, new_status, new_header, m, class, name, keyname, server, have_mark, mark, keycount, validatecount);
break;
}
@@ -1947,9 +1980,10 @@ unsigned char *tcp_request(int confd, time_t now,
#ifdef HAVE_DNSSEC
if (option_bool(OPT_DNSSEC_VALID) && !checking_disabled && (last_server->flags & SERV_DO_DNSSEC))
{
- int keycount = DNSSEC_WORK; /* Limit to number of DNSSEC questions, to catch loops and avoid filling cache. */
+ int keycount = daemon->limit[LIMIT_WORK]; /* Limit to number of DNSSEC questions, to catch loops and avoid filling cache. */
+ int validatecount = daemon->limit[LIMIT_CRYPTO];
int status = tcp_key_recurse(now, STAT_OK, header, m, 0, daemon->namebuff, daemon->keyname,
- last_server, have_mark, mark, &keycount);
+ last_server, have_mark, mark, &keycount, &validatecount);
char *result, *domain = "result";
if (status == STAT_ABANDONED)
diff --git a/src/option.c b/src/option.c
index 4e54afb..944a482 100644
--- a/src/option.c
+++ b/src/option.c
@@ -160,6 +160,7 @@ struct myoption {
#define LOPT_DHCPTTL 348
#define LOPT_TFTP_MTU 349
#define LOPT_REPLY_DELAY 350
+#define LOPT_DNSSEC_LIMITS 351
#ifdef HAVE_GETOPT_LONG
static const struct option opts[] =
@@ -313,6 +314,7 @@ static const struct myoption opts[] =
{ "dnssec-check-unsigned", 0, 0, LOPT_DNSSEC_CHECK },
{ "dnssec-no-timecheck", 0, 0, LOPT_DNSSEC_TIME },
{ "dnssec-timestamp", 1, 0, LOPT_DNSSEC_STAMP },
+ { "dnssec-limits", 1, 0, LOPT_DNSSEC_LIMITS },
#ifdef OPTION6_PREFIX_CLASS
{ "dhcp-prefix-class", 1, 0, LOPT_PREF_CLSS },
#endif
@@ -485,6 +487,7 @@ static struct {
{ LOPT_DNSSEC_CHECK, OPT_DNSSEC_NO_SIGN, NULL, gettext_noop("Ensure answers without DNSSEC are in unsigned zones."), NULL },
{ LOPT_DNSSEC_TIME, OPT_DNSSEC_TIME, NULL, gettext_noop("Don't check DNSSEC signature timestamps until first cache-reload"), NULL },
{ LOPT_DNSSEC_STAMP, ARG_ONE, "<path>", gettext_noop("Timestamp file to verify system clock for DNSSEC"), NULL },
+ { LOPT_DNSSEC_LIMITS, ARG_ONE, "<limit>,..", gettext_noop("Set resource limits for DNSSEC validation"), NULL },
#ifdef OPTION6_PREFIX_CLASS
{ LOPT_PREF_CLSS, ARG_DUP, "set:tag,<class>", gettext_noop("Specify DHCPv6 prefix class"), NULL },
#endif
@@ -4353,6 +4356,23 @@ err:
}
#ifdef HAVE_DNSSEC
+ case LOPT_DNSSEC_LIMITS:
+ {
+ int lim, val;
+
+ for (lim = LIMIT_SIG_FAIL; arg && lim < LIMIT_MAX ; lim++, arg = comma)
+ {
+ comma = split(arg);
+
+ if (!atoi_check(arg, &val))
+ ret_err(gen_err);
+
+ if (val != 0)
+ daemon->limit[lim] = val;
+ }
+
+ break;
+ }
case LOPT_DNSSEC_STAMP:
daemon->timestamp_file = opt_string_alloc(arg);
break;
@@ -4903,6 +4923,12 @@ void read_opts(int argc, char **argv, char *compile_opts)
daemon->soa_refresh = SOA_REFRESH;
daemon->soa_retry = SOA_RETRY;
daemon->soa_expiry = SOA_EXPIRY;
+#ifdef HAVE_DNSSEC
+ daemon->limit[LIMIT_SIG_FAIL] = DNSSEC_LIMIT_SIG_FAIL;
+ daemon->limit[LIMIT_CRYPTO] = DNSSEC_LIMIT_CRYPTO;
+ daemon->limit[LIMIT_WORK] = DNSSEC_LIMIT_WORK;
+ daemon->limit[LIMIT_NSEC3_ITERS] = DNSSEC_LIMIT_NSEC3_ITERS;
+#endif
daemon->max_port = MAX_PORT;
daemon->min_port = MIN_PORT;
diff --git a/src/rrfilter.c b/src/rrfilter.c
index f5b9c61..b332f29 100644
--- a/src/rrfilter.c
+++ b/src/rrfilter.c
@@ -136,9 +136,9 @@ static int check_rrs(unsigned char *p, struct dns_header *header, size_t plen, i
if (class == C_IN)
{
- u16 *d;
+ short *d;
- for (pp = p, d = rrfilter_desc(type); *d != (u16)-1; d++)
+ for (pp = p, d = rrfilter_desc(type); *d != -1; d++)
{
if (*d != 0)
pp += *d;
@@ -266,7 +266,7 @@ size_t rrfilter(struct dns_header *header, size_t plen, int mode)
}
/* This is used in the DNSSEC code too, hence it's exported */
-u16 *rrfilter_desc(int type)
+short *rrfilter_desc(int type)
{
/* List of RRtypes which include domains in the data.
0 -> domain
@@ -277,7 +277,7 @@ u16 *rrfilter_desc(int type)
anything which needs no mangling.
*/
- static u16 rr_desc[] =
+ static short rr_desc[] =
{
T_NS, 0, -1,
T_MD, 0, -1,
@@ -302,10 +302,10 @@ u16 *rrfilter_desc(int type)
0, -1 /* wildcard/catchall */
};
- u16 *p = rr_desc;
+ short *p = rr_desc;
while (*p != type && *p != 0)
- while (*p++ != (u16)-1);
+ while (*p++ != -1);
return p+1;
}