2319 lines
82 KiB
Diff
2319 lines
82 KiB
Diff
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;
|
|
}
|