From e7ff0e0b391341bdc4d9c08dff1c477e1df6a682 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 21 Dec 2015 19:57:34 +0100 Subject: resolved: properly implement RRSIG validation of wildcarded RRsets Note that this is still not complete, one additional step is still missing: when we verified that a wildcard RRset is properly signed, we still need to do an NSEC/NSEC3 proof that no more specific RRset exists. --- src/resolve/resolved-dns-dnssec.c | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) (limited to 'src/resolve/resolved-dns-dnssec.c') diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c index 814cb1c0f9..9ddad38fa6 100644 --- a/src/resolve/resolved-dns-dnssec.c +++ b/src/resolve/resolved-dns-dnssec.c @@ -384,10 +384,17 @@ int dnssec_verify_rrset( gcry_md_write(md, wire_format_name, r); for (k = 0; k < n; k++) { + const char *suffix; size_t l; rr = list[k]; - r = dns_name_to_wire_format(DNS_RESOURCE_KEY_NAME(rr->key), wire_format_name, sizeof(wire_format_name), true); + r = dns_name_suffix(DNS_RESOURCE_KEY_NAME(rr->key), rrsig->rrsig.labels, &suffix); + if (r < 0) + goto finish; + if (r > 0) /* This is a wildcard! */ + gcry_md_write(md, (uint8_t[]) { 1, '*'}, 2); + + r = dns_name_to_wire_format(suffix, wire_format_name, sizeof(wire_format_name), true); if (r < 0) goto finish; gcry_md_write(md, wire_format_name, r); @@ -497,6 +504,8 @@ int dnssec_rrsig_match_dnskey(DnsResourceRecord *rrsig, DnsResourceRecord *dnske } int dnssec_key_match_rrsig(const DnsResourceKey *key, DnsResourceRecord *rrsig) { + int r; + assert(key); assert(rrsig); @@ -509,6 +518,18 @@ int dnssec_key_match_rrsig(const DnsResourceKey *key, DnsResourceRecord *rrsig) if (rrsig->rrsig.type_covered != key->type) return 0; + /* Make sure signer is a parent of the RRset */ + r = dns_name_endswith(DNS_RESOURCE_KEY_NAME(rrsig->key), rrsig->rrsig.signer); + if (r <= 0) + return r; + + /* Make sure the owner name has at least as many labels as the "label" fields indicates. */ + r = dns_name_count_labels(DNS_RESOURCE_KEY_NAME(rrsig->key)); + if (r < 0) + return r; + if (r < rrsig->rrsig.labels) + return 0; + return dns_name_equal(DNS_RESOURCE_KEY_NAME(rrsig->key), DNS_RESOURCE_KEY_NAME(key)); } -- cgit v1.2.3-54-g00ecf From 13b78323bad1e41e0474b833da2a0b72aab56f09 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 21 Dec 2015 20:00:34 +0100 Subject: resolved: when doing NSEC3 proof, first find right NSEC3 suffix When doing an NSEC3 proof, before detrmining whether a name is the closest encloser we first need to figure out the longest common suffix we have with any NSEC3 RR in the reply. --- src/resolve/resolved-dns-dnssec.c | 47 ++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) (limited to 'src/resolve/resolved-dns-dnssec.c') diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c index 9ddad38fa6..dbda71b03d 100644 --- a/src/resolve/resolved-dns-dnssec.c +++ b/src/resolve/resolved-dns-dnssec.c @@ -912,7 +912,7 @@ finish: static int dnssec_test_nsec3(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result) { _cleanup_free_ char *next_closer_domain = NULL, *l = NULL; uint8_t hashed[DNSSEC_HASH_SIZE_MAX]; - const char *p, *pp = NULL; + const char *suffix, *p, *pp = NULL; DnsResourceRecord *rr; DnsAnswerFlags flags; int hashed_size, r; @@ -920,7 +920,42 @@ static int dnssec_test_nsec3(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecR assert(key); assert(result); - /* First step, look for the closest encloser NSEC3 RR in 'answer' that matches 'key' */ + /* First step, look for the longest common suffix we find with any NSEC3 RR in the response. */ + suffix = DNS_RESOURCE_KEY_NAME(key); + for (;;) { + DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) { + _cleanup_free_ char *hashed_domain = NULL, *label = NULL; + + if ((flags & DNS_ANSWER_AUTHENTICATED) == 0) + continue; + + if (rr->key->type != DNS_TYPE_NSEC3) + continue; + + /* RFC 5155, Section 8.2 says we MUST ignore NSEC3 RRs with flags != 0 or 1 */ + if (!IN_SET(rr->nsec3.flags, 0, 1)) + continue; + + r = dns_name_endswith(DNS_RESOURCE_KEY_NAME(rr->key), suffix); + if (r < 0) + return r; + if (r > 0) + goto found_suffix; + } + + /* Strip one label from the front */ + r = dns_name_parent(&suffix); + if (r < 0) + return r; + if (r == 0) + break; + } + + *result = DNSSEC_NSEC_NO_RR; + return 0; + +found_suffix: + /* Second step, find the closest encloser NSEC3 RR in 'answer' that matches 'key' */ p = DNS_RESOURCE_KEY_NAME(key); for (;;) { DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) { @@ -936,7 +971,7 @@ static int dnssec_test_nsec3(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecR if (!IN_SET(rr->nsec3.flags, 0, 1)) continue; - r = dns_name_endswith(DNS_RESOURCE_KEY_NAME(rr->key), p); + r = dns_name_endswith(DNS_RESOURCE_KEY_NAME(rr->key), suffix); if (r < 0) return r; if (r == 0) @@ -956,7 +991,7 @@ static int dnssec_test_nsec3(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecR if (!label) return -ENOMEM; - hashed_domain = strjoin(label, ".", p, NULL); + hashed_domain = strjoin(label, ".", suffix, NULL); if (!hashed_domain) return -ENOMEM; @@ -964,7 +999,7 @@ static int dnssec_test_nsec3(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecR if (r < 0) return r; if (r > 0) - goto found; + goto found_closest_encloser; } /* We didn't find the closest encloser with this name, @@ -984,7 +1019,7 @@ static int dnssec_test_nsec3(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecR *result = DNSSEC_NSEC_NO_RR; return 0; -found: +found_closest_encloser: /* We found a closest encloser in 'p'; next closer is 'pp' */ /* Ensure this is not a DNAME domain, see RFC5155, section 8.3. */ -- cgit v1.2.3-54-g00ecf From db5b0e92b3c23e6f360bd0f44a655b35921a6c98 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 21 Dec 2015 21:06:29 +0100 Subject: resolved: tighten search for NSEC3 RRs a bit Be stricter when searching suitable NSEC3 RRs for proof: generalize the check we use to find suitable NSEC3 RRs, in nsec3_is_good(), and add additional checks, such as checking whether all NSEC3 RRs use the same parameters, have the same suffix and so on. --- src/resolve/resolved-dns-dnssec.c | 134 +++++++++++++++++++-------------- src/resolve/resolved-dns-transaction.c | 2 +- src/shared/dns-domain.c | 18 +++++ src/shared/dns-domain.h | 2 + src/test/test-dns-domain.c | 32 ++++++++ 5 files changed, 131 insertions(+), 57 deletions(-) (limited to 'src/resolve/resolved-dns-dnssec.c') diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c index dbda71b03d..edd1efc739 100644 --- a/src/resolve/resolved-dns-dnssec.c +++ b/src/resolve/resolved-dns-dnssec.c @@ -909,11 +909,63 @@ finish: return r; } +static int nsec3_is_good(DnsResourceRecord *rr, DnsAnswerFlags flags, DnsResourceRecord *nsec3) { + const char *a, *b; + int r; + + assert(rr); + + if ((flags & DNS_ANSWER_AUTHENTICATED) == 0) + return 0; + + if (rr->key->type != DNS_TYPE_NSEC3) + return 0; + + /* RFC 5155, Section 8.2 says we MUST ignore NSEC3 RRs with flags != 0 or 1 */ + if (!IN_SET(rr->nsec3.flags, 0, 1)) + return 0; + + if (!nsec3) + return 1; + + /* If a second NSEC3 RR is specified, also check if they are from the same zone. */ + + if (nsec3 == rr) /* Shortcut */ + return 1; + + if (rr->key->class != nsec3->key->class) + return 0; + if (rr->nsec3.algorithm != nsec3->nsec3.algorithm) + return 0; + if (rr->nsec3.iterations != nsec3->nsec3.iterations) + return 0; + if (rr->nsec3.salt_size != nsec3->nsec3.salt_size) + return 0; + if (memcmp(rr->nsec3.salt, nsec3->nsec3.salt, rr->nsec3.salt_size) != 0) + return 0; + + a = DNS_RESOURCE_KEY_NAME(rr->key); + r = dns_name_parent(&a); /* strip off hash */ + if (r < 0) + return r; + if (r == 0) + return 0; + + b = DNS_RESOURCE_KEY_NAME(nsec3->key); + r = dns_name_parent(&b); /* strip off hash */ + if (r < 0) + return r; + if (r == 0) + return 0; + + return dns_name_equal(a, b); +} + static int dnssec_test_nsec3(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result) { _cleanup_free_ char *next_closer_domain = NULL, *l = NULL; uint8_t hashed[DNSSEC_HASH_SIZE_MAX]; const char *suffix, *p, *pp = NULL; - DnsResourceRecord *rr; + DnsResourceRecord *rr, *suffix_rr; DnsAnswerFlags flags; int hashed_size, r; @@ -923,20 +975,16 @@ static int dnssec_test_nsec3(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecR /* First step, look for the longest common suffix we find with any NSEC3 RR in the response. */ suffix = DNS_RESOURCE_KEY_NAME(key); for (;;) { - DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) { + DNS_ANSWER_FOREACH_FLAGS(suffix_rr, flags, answer) { _cleanup_free_ char *hashed_domain = NULL, *label = NULL; - if ((flags & DNS_ANSWER_AUTHENTICATED) == 0) - continue; - - if (rr->key->type != DNS_TYPE_NSEC3) - continue; - - /* RFC 5155, Section 8.2 says we MUST ignore NSEC3 RRs with flags != 0 or 1 */ - if (!IN_SET(rr->nsec3.flags, 0, 1)) + r = nsec3_is_good(suffix_rr, flags, NULL); + if (r < 0) + return r; + if (r == 0) continue; - r = dns_name_endswith(DNS_RESOURCE_KEY_NAME(rr->key), suffix); + r = dns_name_equal_skip(DNS_RESOURCE_KEY_NAME(suffix_rr->key), 1, suffix); if (r < 0) return r; if (r > 0) @@ -958,42 +1006,34 @@ found_suffix: /* Second step, find the closest encloser NSEC3 RR in 'answer' that matches 'key' */ p = DNS_RESOURCE_KEY_NAME(key); for (;;) { - DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) { - _cleanup_free_ char *hashed_domain = NULL, *label = NULL; + _cleanup_free_ char *hashed_domain = NULL, *label = NULL; - if ((flags & DNS_ANSWER_AUTHENTICATED) == 0) - continue; + hashed_size = dnssec_nsec3_hash(suffix_rr, p, hashed); + if (hashed_size == -EOPNOTSUPP) { + *result = DNSSEC_NSEC_UNSUPPORTED_ALGORITHM; + return 0; + } + if (hashed_size < 0) + return hashed_size; - if (rr->key->type != DNS_TYPE_NSEC3) - continue; + label = base32hexmem(hashed, hashed_size, false); + if (!label) + return -ENOMEM; - /* RFC 5155, Section 8.2 says we MUST ignore NSEC3 RRs with flags != 0 or 1 */ - if (!IN_SET(rr->nsec3.flags, 0, 1)) - continue; + hashed_domain = strjoin(label, ".", suffix, NULL); + if (!hashed_domain) + return -ENOMEM; + + DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) { - r = dns_name_endswith(DNS_RESOURCE_KEY_NAME(rr->key), suffix); + r = nsec3_is_good(rr, flags, suffix_rr); if (r < 0) return r; if (r == 0) continue; - hashed_size = dnssec_nsec3_hash(rr, p, hashed); - if (hashed_size == -EOPNOTSUPP) { - *result = DNSSEC_NSEC_UNSUPPORTED_ALGORITHM; - return 0; - } - if (hashed_size < 0) - return hashed_size; if (rr->nsec3.next_hashed_name_size != (size_t) hashed_size) - return -EBADMSG; - - label = base32hexmem(hashed, hashed_size, false); - if (!label) - return -ENOMEM; - - hashed_domain = strjoin(label, ".", suffix, NULL); - if (!hashed_domain) - return -ENOMEM; + continue; r = dns_name_equal(DNS_RESOURCE_KEY_NAME(rr->key), hashed_domain); if (r < 0) @@ -1056,26 +1096,8 @@ found_closest_encloser: DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) { _cleanup_free_ char *label = NULL, *next_hashed_domain = NULL; - const char *nsec3_parent; - - if ((flags & DNS_ANSWER_AUTHENTICATED) == 0) - continue; - - if (rr->key->type != DNS_TYPE_NSEC3) - continue; - - /* RFC 5155, Section 8.2 says we MUST ignore NSEC3 RRs with flags != 0 or 1 */ - if (!IN_SET(rr->nsec3.flags, 0, 1)) - continue; - - nsec3_parent = DNS_RESOURCE_KEY_NAME(rr->key); - r = dns_name_parent(&nsec3_parent); - if (r < 0) - return r; - if (r == 0) - continue; - r = dns_name_equal(p, nsec3_parent); + r = nsec3_is_good(rr, flags, suffix_rr); if (r < 0) return r; if (r == 0) diff --git a/src/resolve/resolved-dns-transaction.c b/src/resolve/resolved-dns-transaction.c index d2aec13264..1d02d7e942 100644 --- a/src/resolve/resolved-dns-transaction.c +++ b/src/resolve/resolved-dns-transaction.c @@ -1516,7 +1516,7 @@ int dns_transaction_request_dnssec_keys(DnsTransaction *t) { if (!soa) return -ENOMEM; - log_debug("Requesting SOA to validate transaction %" PRIu16 " (%s, unsigned non-SOA/NS RRset).", t->id, DNS_RESOURCE_KEY_NAME(rr->key)); + log_debug("Requesting SOA to validate transaction %" PRIu16 " (%s, unsigned non-SOA/NS RRset <%s>).", t->id, DNS_RESOURCE_KEY_NAME(rr->key), dns_resource_record_to_string(rr)); r = dns_transaction_request_dnssec_rr(t, soa); if (r < 0) return r; diff --git a/src/shared/dns-domain.c b/src/shared/dns-domain.c index f3dbf60395..f44e80c9db 100644 --- a/src/shared/dns-domain.c +++ b/src/shared/dns-domain.c @@ -1215,3 +1215,21 @@ int dns_name_count_labels(const char *name) { return (int) n; } + +int dns_name_equal_skip(const char *a, unsigned n_labels, const char *b) { + int r; + + assert(a); + assert(b); + + while (n_labels > 0) { + + r = dns_name_parent(&a); + if (r <= 0) + return r; + + n_labels --; + } + + return dns_name_equal(a, b); +} diff --git a/src/shared/dns-domain.h b/src/shared/dns-domain.h index 7b509729fb..dd8ae3ac98 100644 --- a/src/shared/dns-domain.h +++ b/src/shared/dns-domain.h @@ -102,3 +102,5 @@ int dns_service_split(const char *joined, char **name, char **type, char **domai int dns_name_suffix(const char *name, unsigned n_labels, const char **ret); int dns_name_count_labels(const char *name); + +int dns_name_equal_skip(const char *a, unsigned n_labels, const char *b); diff --git a/src/test/test-dns-domain.c b/src/test/test-dns-domain.c index 3a15ea5c06..78c5631ff1 100644 --- a/src/test/test-dns-domain.c +++ b/src/test/test-dns-domain.c @@ -515,6 +515,37 @@ static void test_dns_name_count_labels(void) { test_dns_name_count_labels_one("..", -EINVAL); } +static void test_dns_name_equal_skip_one(const char *a, unsigned n_labels, const char *b, int ret) { + assert_se(dns_name_equal_skip(a, n_labels, b) == ret); +} + +static void test_dns_name_equal_skip(void) { + test_dns_name_equal_skip_one("foo", 0, "bar", 0); + test_dns_name_equal_skip_one("foo", 0, "foo", 1); + test_dns_name_equal_skip_one("foo", 1, "foo", 0); + test_dns_name_equal_skip_one("foo", 2, "foo", 0); + + test_dns_name_equal_skip_one("foo.bar", 0, "foo.bar", 1); + test_dns_name_equal_skip_one("foo.bar", 1, "foo.bar", 0); + test_dns_name_equal_skip_one("foo.bar", 2, "foo.bar", 0); + test_dns_name_equal_skip_one("foo.bar", 3, "foo.bar", 0); + + test_dns_name_equal_skip_one("foo.bar", 0, "bar", 0); + test_dns_name_equal_skip_one("foo.bar", 1, "bar", 1); + test_dns_name_equal_skip_one("foo.bar", 2, "bar", 0); + test_dns_name_equal_skip_one("foo.bar", 3, "bar", 0); + + test_dns_name_equal_skip_one("foo.bar", 0, "", 0); + test_dns_name_equal_skip_one("foo.bar", 1, "", 0); + test_dns_name_equal_skip_one("foo.bar", 2, "", 1); + test_dns_name_equal_skip_one("foo.bar", 3, "", 0); + + test_dns_name_equal_skip_one("", 0, "", 1); + test_dns_name_equal_skip_one("", 1, "", 0); + test_dns_name_equal_skip_one("", 1, "foo", 0); + test_dns_name_equal_skip_one("", 2, "foo", 0); +} + int main(int argc, char *argv[]) { test_dns_label_unescape(); @@ -537,6 +568,7 @@ int main(int argc, char *argv[]) { test_dns_name_change_suffix(); test_dns_name_suffix(); test_dns_name_count_labels(); + test_dns_name_equal_skip(); return 0; } -- cgit v1.2.3-54-g00ecf From 3ecc3df8ffa2d40c814fd6c1c4d554994d60b7af Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 21 Dec 2015 22:07:41 +0100 Subject: update DNSSEC TODO --- src/resolve/resolved-dns-dnssec.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/resolve/resolved-dns-dnssec.c') diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c index edd1efc739..482ee4a0b3 100644 --- a/src/resolve/resolved-dns-dnssec.c +++ b/src/resolve/resolved-dns-dnssec.c @@ -36,7 +36,7 @@ * TODO: * * - Make trust anchor store read additional DS+DNSKEY data from disk - * - wildcard zones compatibility + * - wildcard zones compatibility (NSEC/NSEC3 wildcard check is missing) * - multi-label zone compatibility * - cname/dname compatibility * - per-interface DNSSEC setting -- cgit v1.2.3-54-g00ecf From d1c4ee32480cb997b673ca8396ca95c70be610f7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 22 Dec 2015 18:20:09 +0100 Subject: resolved: be stricter when searching for a DS RR for a DNSKEY RR --- src/resolve/resolved-dns-dnssec.c | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'src/resolve/resolved-dns-dnssec.c') diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c index 482ee4a0b3..f37f1d91be 100644 --- a/src/resolve/resolved-dns-dnssec.c +++ b/src/resolve/resolved-dns-dnssec.c @@ -831,6 +831,15 @@ int dnssec_verify_dnskey_search(DnsResourceRecord *dnskey, DnsAnswer *validated_ if (ds->key->type != DNS_TYPE_DS) continue; + if (ds->key->class != dnskey->key->class) + continue; + + r = dns_name_equal(DNS_RESOURCE_KEY_NAME(dnskey->key), DNS_RESOURCE_KEY_NAME(ds->key)); + if (r < 0) + return r; + if (r == 0) + continue; + r = dnssec_verify_dnskey(dnskey, ds); if (r < 0) return r; -- cgit v1.2.3-54-g00ecf From ed29bfdce6ef8b1c6e14bb4e77e19e7048f35dd4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 22 Dec 2015 18:22:19 +0100 Subject: resolved: if we accepted unauthenticated NSEC/NSEC3 RRs, use them for proofs But keep track that the proof is not authenticated. --- src/resolve/resolved-dns-dnssec.c | 23 +++++++++++++---------- src/resolve/resolved-dns-dnssec.h | 2 +- src/resolve/resolved-dns-transaction.c | 7 ++++--- 3 files changed, 18 insertions(+), 14 deletions(-) (limited to 'src/resolve/resolved-dns-dnssec.c') diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c index f37f1d91be..14fa58c544 100644 --- a/src/resolve/resolved-dns-dnssec.c +++ b/src/resolve/resolved-dns-dnssec.c @@ -924,9 +924,6 @@ static int nsec3_is_good(DnsResourceRecord *rr, DnsAnswerFlags flags, DnsResourc assert(rr); - if ((flags & DNS_ANSWER_AUTHENTICATED) == 0) - return 0; - if (rr->key->type != DNS_TYPE_NSEC3) return 0; @@ -970,16 +967,18 @@ static int nsec3_is_good(DnsResourceRecord *rr, DnsAnswerFlags flags, DnsResourc return dns_name_equal(a, b); } -static int dnssec_test_nsec3(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result) { +static int dnssec_test_nsec3(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result, bool *authenticated) { _cleanup_free_ char *next_closer_domain = NULL, *l = NULL; uint8_t hashed[DNSSEC_HASH_SIZE_MAX]; const char *suffix, *p, *pp = NULL; DnsResourceRecord *rr, *suffix_rr; DnsAnswerFlags flags; int hashed_size, r; + bool a; assert(key); assert(result); + assert(authenticated); /* First step, look for the longest common suffix we find with any NSEC3 RR in the response. */ suffix = DNS_RESOURCE_KEY_NAME(key); @@ -1047,8 +1046,10 @@ found_suffix: r = dns_name_equal(DNS_RESOURCE_KEY_NAME(rr->key), hashed_domain); if (r < 0) return r; - if (r > 0) + if (r > 0) { + a = flags & DNS_ANSWER_AUTHENTICATED; goto found_closest_encloser; + } } /* We didn't find the closest encloser with this name, @@ -1086,6 +1087,7 @@ found_closest_encloser: if (!pp) { /* No next closer NSEC3 RR. That means there's a direct NSEC3 RR for our key. */ *result = bitmap_isset(rr->nsec3.types, key->type) ? DNSSEC_NSEC_FOUND : DNSSEC_NSEC_NODATA; + *authenticated = a; return 0; } @@ -1129,6 +1131,7 @@ found_closest_encloser: else *result = DNSSEC_NSEC_NXDOMAIN; + *authenticated = a && (flags & DNS_ANSWER_AUTHENTICATED); return 1; } } @@ -1137,7 +1140,7 @@ found_closest_encloser: return 0; } -int dnssec_test_nsec(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result) { +int dnssec_test_nsec(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result, bool *authenticated) { DnsResourceRecord *rr; bool have_nsec3 = false; DnsAnswerFlags flags; @@ -1145,6 +1148,7 @@ int dnssec_test_nsec(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *r assert(key); assert(result); + assert(authenticated); /* Look for any NSEC/NSEC3 RRs that say something about the specified key. */ @@ -1153,9 +1157,6 @@ int dnssec_test_nsec(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *r if (rr->key->class != key->class) continue; - if ((flags & DNS_ANSWER_AUTHENTICATED) == 0) - continue; - switch (rr->key->type) { case DNS_TYPE_NSEC: @@ -1165,6 +1166,7 @@ int dnssec_test_nsec(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *r return r; if (r > 0) { *result = bitmap_isset(rr->nsec.types, key->type) ? DNSSEC_NSEC_FOUND : DNSSEC_NSEC_NODATA; + *authenticated = flags & DNS_ANSWER_AUTHENTICATED; return 0; } @@ -1173,6 +1175,7 @@ int dnssec_test_nsec(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *r return r; if (r > 0) { *result = DNSSEC_NSEC_NXDOMAIN; + *authenticated = flags & DNS_ANSWER_AUTHENTICATED; return 0; } break; @@ -1185,7 +1188,7 @@ int dnssec_test_nsec(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *r /* OK, this was not sufficient. Let's see if NSEC3 can help. */ if (have_nsec3) - return dnssec_test_nsec3(answer, key, result); + return dnssec_test_nsec3(answer, key, result, authenticated); /* No approproate NSEC RR found, report this. */ *result = DNSSEC_NSEC_NO_RR; diff --git a/src/resolve/resolved-dns-dnssec.h b/src/resolve/resolved-dns-dnssec.h index d17d5142f5..9ad20c8c69 100644 --- a/src/resolve/resolved-dns-dnssec.h +++ b/src/resolve/resolved-dns-dnssec.h @@ -89,7 +89,7 @@ typedef enum DnssecNsecResult { DNSSEC_NSEC_OPTOUT, } DnssecNsecResult; -int dnssec_test_nsec(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result); +int dnssec_test_nsec(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result, bool *authenticated); const char* dnssec_mode_to_string(DnssecMode m) _const_; DnssecMode dnssec_mode_from_string(const char *s) _pure_; diff --git a/src/resolve/resolved-dns-transaction.c b/src/resolve/resolved-dns-transaction.c index 9ac9de01ad..0f48f9bf81 100644 --- a/src/resolve/resolved-dns-transaction.c +++ b/src/resolve/resolved-dns-transaction.c @@ -2112,9 +2112,10 @@ int dns_transaction_validate_dnssec(DnsTransaction *t) { } else if (r == 0) { DnssecNsecResult nr; + bool authenticated = false; /* Bummer! Let's check NSEC/NSEC3 */ - r = dnssec_test_nsec(t->answer, t->key, &nr); + r = dnssec_test_nsec(t->answer, t->key, &nr, &authenticated); if (r < 0) return r; @@ -2125,7 +2126,7 @@ int dns_transaction_validate_dnssec(DnsTransaction *t) { log_debug("Proved NXDOMAIN via NSEC/NSEC3 for transaction %u (%s)", t->id, dns_transaction_key_string(t)); t->answer_dnssec_result = DNSSEC_VALIDATED; t->answer_rcode = DNS_RCODE_NXDOMAIN; - t->answer_authenticated = true; + t->answer_authenticated = authenticated; break; case DNSSEC_NSEC_NODATA: @@ -2133,7 +2134,7 @@ int dns_transaction_validate_dnssec(DnsTransaction *t) { log_debug("Proved NODATA via NSEC/NSEC3 for transaction %u (%s)", t->id, dns_transaction_key_string(t)); t->answer_dnssec_result = DNSSEC_VALIDATED; t->answer_rcode = DNS_RCODE_SUCCESS; - t->answer_authenticated = true; + t->answer_authenticated = authenticated; break; case DNSSEC_NSEC_OPTOUT: -- cgit v1.2.3-54-g00ecf From b652d4a2099d1c167584dcc1d179d47c58dc38a2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 25 Dec 2015 15:05:46 +0100 Subject: resolved: add an automatic downgrade to non-DNSSEC mode This adds a mode that makes resolved automatically downgrade from DNSSEC support to classic non-DNSSEC resolving if the configured DNS server is not capable of DNSSEC. Enabling this mode increases compatibility with crappy network equipment, but of course opens up the system to downgrading attacks. The new mode can be enabled by setting DNSSEC=downgrade-ok in resolved.conf. DNSSEC=yes otoh remains a "strict" mode, where DNS resolving rather fails then allow downgrading. Downgrading is done: - when the server does not support EDNS0+DO - or when the server supports it but does not augment returned RRs with RRSIGs. The latter is detected when requesting DS or SOA RRs for the root domain (which is necessary to do proofs for unsigned data) --- src/resolve/resolved-dns-dnssec.c | 2 + src/resolve/resolved-dns-dnssec.h | 11 ++++- src/resolve/resolved-dns-server.c | 20 +++++++-- src/resolve/resolved-dns-server.h | 12 +++++- src/resolve/resolved-dns-transaction.c | 75 ++++++++++++++++++++++++++++++---- 5 files changed, 106 insertions(+), 14 deletions(-) (limited to 'src/resolve/resolved-dns-dnssec.c') diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c index 14fa58c544..a856f0717e 100644 --- a/src/resolve/resolved-dns-dnssec.c +++ b/src/resolve/resolved-dns-dnssec.c @@ -1197,6 +1197,7 @@ int dnssec_test_nsec(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *r static const char* const dnssec_mode_table[_DNSSEC_MODE_MAX] = { [DNSSEC_NO] = "no", + [DNSSEC_DOWNGRADE_OK] = "downgrade-ok", [DNSSEC_YES] = "yes", }; DEFINE_STRING_TABLE_LOOKUP(dnssec_mode, DnssecMode); @@ -1211,5 +1212,6 @@ static const char* const dnssec_result_table[_DNSSEC_RESULT_MAX] = { [DNSSEC_UNSIGNED] = "unsigned", [DNSSEC_FAILED_AUXILIARY] = "failed-auxiliary", [DNSSEC_NSEC_MISMATCH] = "nsec-mismatch", + [DNSSEC_INCOMPATIBLE_SERVER] = "incompatible-server", }; DEFINE_STRING_TABLE_LOOKUP(dnssec_result, DnssecResult); diff --git a/src/resolve/resolved-dns-dnssec.h b/src/resolve/resolved-dns-dnssec.h index 9ad20c8c69..d7aecbce13 100644 --- a/src/resolve/resolved-dns-dnssec.h +++ b/src/resolve/resolved-dns-dnssec.h @@ -32,7 +32,14 @@ enum DnssecMode { /* No DNSSEC validation is done */ DNSSEC_NO, - /* Validate locally, if the server knows DO, but if not, don't. Don't trust the AD bit */ + /* Validate locally, if the server knows DO, but if not, + * don't. Don't trust the AD bit. If the server doesn't do + * DNSSEC properly, downgrade to non-DNSSEC operation. Of + * course, we then are vulnerable to a downgrade attack, but + * that's life and what is configured. */ + DNSSEC_DOWNGRADE_OK, + + /* Insist on DNSSEC server support, and rather fail than downgrading. */ DNSSEC_YES, _DNSSEC_MODE_MAX, @@ -54,6 +61,8 @@ enum DnssecResult { DNSSEC_UNSIGNED, DNSSEC_FAILED_AUXILIARY, DNSSEC_NSEC_MISMATCH, + DNSSEC_INCOMPATIBLE_SERVER, + _DNSSEC_RESULT_MAX, _DNSSEC_RESULT_INVALID = -1 }; diff --git a/src/resolve/resolved-dns-server.c b/src/resolve/resolved-dns-server.c index d565f99c09..b0db5bbb16 100644 --- a/src/resolve/resolved-dns-server.c +++ b/src/resolve/resolved-dns-server.c @@ -228,9 +228,11 @@ void dns_server_packet_received(DnsServer *s, DnsServerFeatureLevel features, us assert(s); if (features == DNS_SERVER_FEATURE_LEVEL_LARGE) { - /* even if we successfully receive a reply to a request announcing - support for large packets, that does not mean we can necessarily - receive large packets. */ + /* Even if we successfully receive a reply to a + request announcing support for large packets, that + does not mean we can necessarily receive large + packets. */ + if (s->verified_features < DNS_SERVER_FEATURE_LEVEL_LARGE - 1) { s->verified_features = DNS_SERVER_FEATURE_LEVEL_LARGE - 1; assert_se(sd_event_now(s->manager->event, clock_boottime_or_monotonic(), &s->verified_usec) >= 0); @@ -278,6 +280,17 @@ void dns_server_packet_failed(DnsServer *s, DnsServerFeatureLevel features) { s->n_failed_attempts = (unsigned) -1; } +void dns_server_packet_rrsig_missing(DnsServer *s) { + _cleanup_free_ char *ip = NULL; + assert(s); + assert(s->manager); + + in_addr_to_string(s->family, &s->address, &ip); + log_warning("DNS server %s does not augment replies with RRSIG records, DNSSEC not available.", strna(ip)); + + s->rrsig_missing = true; +} + static bool dns_server_grace_period_expired(DnsServer *s) { usec_t ts; @@ -307,6 +320,7 @@ DnsServerFeatureLevel dns_server_possible_features(DnsServer *s) { s->possible_features = DNS_SERVER_FEATURE_LEVEL_BEST; s->n_failed_attempts = 0; s->verified_usec = 0; + s->rrsig_missing = false; in_addr_to_string(s->family, &s->address, &ip); log_info("Grace period over, resuming full feature set for DNS server %s", strna(ip)); diff --git a/src/resolve/resolved-dns-server.h b/src/resolve/resolved-dns-server.h index b07fc3af3d..3011904bfd 100644 --- a/src/resolve/resolved-dns-server.h +++ b/src/resolve/resolved-dns-server.h @@ -61,8 +61,6 @@ struct DnsServer { int family; union in_addr_union address; - bool marked:1; - usec_t resend_timeout; usec_t max_rtt; @@ -73,6 +71,15 @@ struct DnsServer { usec_t verified_usec; usec_t features_grace_period_usec; + /* Indicates whether responses are augmented with RRSIG by + * server or not. Note that this is orthogonal to the feature + * level stuff, as it's only information describing responses, + * and has no effect on how the questions are asked. */ + bool rrsig_missing:1; + + /* Used when GC'ing old DNS servers when configuration changes. */ + bool marked:1; + /* If linked is set, then this server appears in the servers linked list */ bool linked:1; LIST_FIELDS(DnsServer, servers); @@ -95,6 +102,7 @@ void dns_server_move_back_and_unmark(DnsServer *s); void dns_server_packet_received(DnsServer *s, DnsServerFeatureLevel features, usec_t rtt, size_t size); void dns_server_packet_lost(DnsServer *s, DnsServerFeatureLevel features, usec_t usec); void dns_server_packet_failed(DnsServer *s, DnsServerFeatureLevel features); +void dns_server_packet_rrsig_missing(DnsServer *s); DnsServer *dns_server_find(DnsServer *first, int family, const union in_addr_union *in_addr); diff --git a/src/resolve/resolved-dns-transaction.c b/src/resolve/resolved-dns-transaction.c index 2875330812..5933e0e462 100644 --- a/src/resolve/resolved-dns-transaction.c +++ b/src/resolve/resolved-dns-transaction.c @@ -478,10 +478,19 @@ static void dns_transaction_process_dnssec(DnsTransaction *t) { return; } + if (t->answer_dnssec_result == DNSSEC_INCOMPATIBLE_SERVER && + t->scope->dnssec_mode == DNSSEC_YES) { + /* We are not in automatic downgrade mode, and the + * server is bad, refuse operation. */ + dns_transaction_complete(t, DNS_TRANSACTION_DNSSEC_FAILED); + return; + } + if (!IN_SET(t->answer_dnssec_result, - _DNSSEC_RESULT_INVALID, /* No DNSSEC validation enabled */ - DNSSEC_VALIDATED, /* Answer is signed and validated successfully */ - DNSSEC_UNSIGNED)) { /* Answer is right-fully unsigned */ + _DNSSEC_RESULT_INVALID, /* No DNSSEC validation enabled */ + DNSSEC_VALIDATED, /* Answer is signed and validated successfully */ + DNSSEC_UNSIGNED, /* Answer is right-fully unsigned */ + DNSSEC_INCOMPATIBLE_SERVER)) { /* Server does not do DNSSEC (Yay, we are downgrade attack vulnerable!) */ dns_transaction_complete(t, DNS_TRANSACTION_DNSSEC_FAILED); return; } @@ -1001,7 +1010,7 @@ static int dns_transaction_make_packet(DnsTransaction *t) { if (t->sent) return 0; - r = dns_packet_new_query(&p, t->scope->protocol, 0, t->scope->dnssec_mode == DNSSEC_YES); + r = dns_packet_new_query(&p, t->scope->protocol, 0, t->scope->dnssec_mode != DNSSEC_NO); if (r < 0) return r; @@ -1336,9 +1345,14 @@ int dns_transaction_request_dnssec_keys(DnsTransaction *t) { * - For other queries with no matching response RRs, and no NSEC/NSEC3, the SOA RR */ - if (t->scope->dnssec_mode != DNSSEC_YES) + if (t->scope->dnssec_mode == DNSSEC_NO) return 0; + if (t->current_features < DNS_SERVER_FEATURE_LEVEL_DO) + return 0; /* Server doesn't do DNSSEC, there's no point in requesting any RRs then. */ + if (t->server && t->server->rrsig_missing) + return 0; /* Server handles DNSSEC requests, but isn't augmenting responses with RRSIGs. No point in trying DNSSEC then. */ + DNS_ANSWER_FOREACH(rr, t->answer) { if (dns_type_is_pseudo(rr->key->type)) @@ -1682,7 +1696,7 @@ static int dns_transaction_requires_rrsig(DnsTransaction *t, DnsResourceRecord * /* Checks if the RR we are looking for must be signed with an * RRSIG. This is used for positive responses. */ - if (t->scope->dnssec_mode != DNSSEC_YES) + if (t->scope->dnssec_mode == DNSSEC_NO) return false; if (dns_type_is_pseudo(rr->key->type)) @@ -1819,7 +1833,7 @@ static int dns_transaction_requires_nsec(DnsTransaction *t) { /* Checks if we need to insist on NSEC/NSEC3 RRs for proving * this negative reply */ - if (t->scope->dnssec_mode != DNSSEC_YES) + if (t->scope->dnssec_mode == DNSSEC_NO) return false; if (dns_type_is_pseudo(t->key->type)) @@ -1932,6 +1946,17 @@ static int dns_transaction_dnskey_authenticated(DnsTransaction *t, DnsResourceRe return found ? false : -ENXIO; } +static int dns_transaction_known_signed(DnsTransaction *t, DnsResourceRecord *rr) { + assert(t); + assert(rr); + + /* We know that the root domain is signed, hence if it appears + * not to be signed, there's a problem with the DNS server */ + + return rr->key->class == DNS_CLASS_IN && + dns_name_is_root(DNS_RESOURCE_KEY_NAME(rr->key)); +} + int dns_transaction_validate_dnssec(DnsTransaction *t) { _cleanup_(dns_answer_unrefp) DnsAnswer *validated = NULL; bool dnskeys_finalized = false; @@ -1945,7 +1970,7 @@ int dns_transaction_validate_dnssec(DnsTransaction *t) { * t->validated_keys, let's see which RRs we can now * authenticate with that. */ - if (t->scope->dnssec_mode != DNSSEC_YES) + if (t->scope->dnssec_mode == DNSSEC_NO) return 0; /* Already validated */ @@ -1963,6 +1988,13 @@ int dns_transaction_validate_dnssec(DnsTransaction *t) { if (t->answer_source != DNS_TRANSACTION_NETWORK) return 0; + if (t->current_features < DNS_SERVER_FEATURE_LEVEL_DO || + (t->server && t->server->rrsig_missing)) { + /* The server does not support DNSSEC, or doesn't augment responses with RRSIGs. */ + t->answer_dnssec_result = DNSSEC_INCOMPATIBLE_SERVER; + return 0; + } + log_debug("Validating response from transaction %" PRIu16 " (%s).", t->id, dns_transaction_key_string(t)); /* First see if there are DNSKEYs we already known a validated DS for. */ @@ -2037,6 +2069,33 @@ int dns_transaction_validate_dnssec(DnsTransaction *t) { changed = true; break; } + + r = dns_transaction_known_signed(t, rr); + if (r < 0) + return r; + if (r > 0) { + /* This is an RR we know has to be signed. If it isn't this means + * the server is not attaching RRSIGs, hence complain. */ + + dns_server_packet_rrsig_missing(t->server); + + if (t->scope->dnssec_mode == DNSSEC_DOWNGRADE_OK) { + + /* Downgrading is OK? If so, just consider the information unsigned */ + + r = dns_answer_move_by_key(&validated, &t->answer, rr->key, 0); + if (r < 0) + return r; + + t->scope->manager->n_dnssec_insecure++; + changed = true; + break; + } + + /* Otherwise, fail */ + t->answer_dnssec_result = DNSSEC_INCOMPATIBLE_SERVER; + return 0; + } } if (IN_SET(result, -- cgit v1.2.3-54-g00ecf