]> git.ipfire.org Git - thirdparty/systemd.git/blobdiff - src/resolve/resolved-dns-dnssec.c
resolved: refuse validating wildcard RRs for SOA, NSEC3, DNAME
[thirdparty/systemd.git] / src / resolve / resolved-dns-dnssec.c
index 32d4834aa1db083c597122806f6631660383ae97..6f0f8f837e702dc2a02f58373980010013e47088 100644 (file)
  *   - multi-label zone compatibility
  *   - cname/dname compatibility
  *   - nxdomain on qname
- *   - per-interface DNSSEC setting
+ *   - bus calls to override DNSEC setting per interface
+ *   - log all DNSSEC downgrades
+ *   - enable by default
  *
+ *   - RFC 4035, Section 5.3.4 (When receiving a positive wildcard reply, use NSEC to ensure it actually really applies)
+ *   - RFC 6840, Section 4.1 (ensure we don't get fed a glue NSEC from the parent zone)
+ *   - RFC 6840, Section 4.3 (check for CNAME on NSEC too)
  * */
 
 #define VERIFY_RRS_MAX 256
@@ -49,8 +54,8 @@
 /* Permit a maximum clock skew of 1h 10min. This should be enough to deal with DST confusion */
 #define SKEW_MAX (1*USEC_PER_HOUR + 10*USEC_PER_MINUTE)
 
-/* Maximum number of NSEC3 iterations we'll do. */
-#define NSEC3_ITERATIONS_MAX 2048
+/* Maximum number of NSEC3 iterations we'll do. RFC5155 says 2500 shall be the maximum useful value */
+#define NSEC3_ITERATIONS_MAX 2500
 
 /*
  * The DNSSEC Chain of trust:
@@ -437,8 +442,9 @@ static int dnssec_rrsig_expired(DnsResourceRecord *rrsig, usec_t realtime) {
         expiration = rrsig->rrsig.expiration * USEC_PER_SEC;
         inception = rrsig->rrsig.inception * USEC_PER_SEC;
 
+        /* Consider inverted validity intervals as expired */
         if (inception > expiration)
-                return -EKEYREJECTED;
+                return true;
 
         /* Permit a certain amount of clock skew of 10% of the valid
          * time range. This takes inspiration from unbound's
@@ -508,6 +514,8 @@ int dnssec_verify_rrset(
         gcry_md_hd_t md = NULL;
         int r, md_algorithm;
         size_t k, n = 0;
+        bool wildcard;
+        const char *source;
 
         assert(key);
         assert(rrsig);
@@ -536,8 +544,30 @@ int dnssec_verify_rrset(
                 return 0;
         }
 
+        /* Determine the "Source of Synthesis" and whether this is a wildcard RRSIG */
+        r = dns_name_suffix(DNS_RESOURCE_KEY_NAME(key), rrsig->rrsig.labels, &source);
+        if (r < 0)
+                return r;
+        if (r > 0 && !dns_type_may_wildcard(rrsig->rrsig.type_covered)) {
+                /* We refuse to validate NSEC3 or SOA RRs that are synthesized from wildcards */
+                *result = DNSSEC_INVALID;
+                return 0;
+        }
+        if (r == 1) {
+                /* If we stripped a single label, then let's see if that maybe was "*". If so, we are not really
+                 * synthesized from a wildcard, we are the wildcard itself. Treat that like a normal name. */
+                r = dns_name_startswith(DNS_RESOURCE_KEY_NAME(key), "*");
+                if (r < 0)
+                        return r;
+                if (r > 0)
+                        source = DNS_RESOURCE_KEY_NAME(key);
+
+                wildcard = r == 0;
+        } else
+                wildcard = r > 0;
+
         /* Collect all relevant RRs in a single array, so that we can look at the RRset */
-        list = newa(DnsResourceRecord *, a->n_rrs);
+        list = newa(DnsResourceRecord *, dns_answer_size(a));
 
         DNS_ANSWER_FOREACH(rr, a) {
                 r = dns_resource_key_equal(key, rr->key);
@@ -586,20 +616,19 @@ int dnssec_verify_rrset(
                 goto finish;
         gcry_md_write(md, wire_format_name, r);
 
+        /* Convert the source of synthesis into wire format */
+        r = dns_name_to_wire_format(source, wire_format_name, sizeof(wire_format_name), true);
+        if (r < 0)
+                goto finish;
+
         for (k = 0; k < n; k++) {
-                const char *suffix;
                 size_t l;
+
                 rr = list[k];
 
-                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! */
+                /* Hash the source of synthesis. If this is a wildcard, then prefix it with the *. label */
+                if (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);
 
                 md_add_uint16(md, rr->key->type);
@@ -646,7 +675,12 @@ int dnssec_verify_rrset(
         if (r < 0)
                 goto finish;
 
-        *result = r ? DNSSEC_VALIDATED : DNSSEC_INVALID;
+        if (!r)
+                *result = DNSSEC_INVALID;
+        else if (wildcard)
+                *result = DNSSEC_VALIDATED_WILDCARD;
+        else
+                *result = DNSSEC_VALIDATED;
         r = 0;
 
 finish:
@@ -743,7 +777,8 @@ int dnssec_verify_rrset_search(
                 const DnsResourceKey *key,
                 DnsAnswer *validated_dnskeys,
                 usec_t realtime,
-                DnssecResult *result) {
+                DnssecResult *result,
+                DnsResourceRecord **ret_rrsig) {
 
         bool found_rrsig = false, found_invalid = false, found_expired_rrsig = false, found_unsupported_algorithm = false;
         DnsResourceRecord *rrsig;
@@ -803,13 +838,17 @@ int dnssec_verify_rrset_search(
                         switch (one_result) {
 
                         case DNSSEC_VALIDATED:
+                        case DNSSEC_VALIDATED_WILDCARD:
                                 /* Yay, the RR has been validated,
                                  * return immediately, but fix up the expiry */
                                 r = dnssec_fix_rrset_ttl(a, key, rrsig, realtime);
                                 if (r < 0)
                                         return r;
 
-                                *result = DNSSEC_VALIDATED;
+                                if (ret_rrsig)
+                                        *ret_rrsig = rrsig;
+
+                                *result = one_result;
                                 return 0;
 
                         case DNSSEC_INVALID:
@@ -854,6 +893,9 @@ int dnssec_verify_rrset_search(
         else
                 *result = DNSSEC_NO_SIGNATURE;
 
+        if (ret_rrsig)
+                *ret_rrsig = NULL;
+
         return 0;
 }
 
@@ -885,8 +927,6 @@ int dnssec_canonicalize(const char *n, char *buffer, size_t buffer_max) {
                 return -ENOBUFS;
 
         for (;;) {
-                size_t i;
-
                 r = dns_label_unescape(&n, buffer, buffer_max);
                 if (r < 0)
                         return r;
@@ -913,11 +953,7 @@ int dnssec_canonicalize(const char *n, char *buffer, size_t buffer_max) {
                 if (memchr(buffer, '.', r))
                         return -EINVAL;
 
-                for (i = 0; i < (size_t) r; i ++) {
-                        if (buffer[i] >= 'A' && buffer[i] <= 'Z')
-                                buffer[i] = buffer[i] - 'A' + 'a';
-                }
-
+                ascii_strlower_n(buffer, (size_t) r);
                 buffer[r] = '.';
 
                 buffer += r + 1;
@@ -1155,7 +1191,7 @@ finish:
         return r;
 }
 
-static int nsec3_is_good(DnsResourceRecord *rr, DnsAnswerFlags flags, DnsResourceRecord *nsec3) {
+static int nsec3_is_good(DnsResourceRecord *rr, DnsResourceRecord *nsec3) {
         const char *a, *b;
         int r;
 
@@ -1211,8 +1247,28 @@ static int nsec3_is_good(DnsResourceRecord *rr, DnsAnswerFlags flags, DnsResourc
         return dns_name_equal(a, b);
 }
 
-static int nsec3_hashed_domain(DnsResourceRecord *nsec3, const char *domain, const char *zone, char **ret) {
-        _cleanup_free_ char *l = NULL, *hashed_domain = NULL;
+static int nsec3_hashed_domain_format(const uint8_t *hashed, size_t hashed_size, const char *zone, char **ret) {
+        _cleanup_free_ char *l = NULL;
+        char *j;
+
+        assert(hashed);
+        assert(hashed_size > 0);
+        assert(zone);
+        assert(ret);
+
+        l = base32hexmem(hashed, hashed_size, false);
+        if (!l)
+                return -ENOMEM;
+
+        j = strjoin(l, ".", zone, NULL);
+        if (!j)
+                return -ENOMEM;
+
+        *ret = j;
+        return (int) hashed_size;
+}
+
+static int nsec3_hashed_domain_make(DnsResourceRecord *nsec3, const char *domain, const char *zone, char **ret) {
         uint8_t hashed[DNSSEC_HASH_SIZE_MAX];
         int hashed_size;
 
@@ -1225,18 +1281,7 @@ static int nsec3_hashed_domain(DnsResourceRecord *nsec3, const char *domain, con
         if (hashed_size < 0)
                 return hashed_size;
 
-        l = base32hexmem(hashed, hashed_size, false);
-        if (!l)
-                return -ENOMEM;
-
-        hashed_domain = strjoin(l, ".", zone, NULL);
-        if (!hashed_domain)
-                return -ENOMEM;
-
-        *ret = hashed_domain;
-        hashed_domain = NULL;
-
-        return hashed_size;
+        return nsec3_hashed_domain_format(hashed, (size_t) hashed_size, zone, ret);
 }
 
 /* See RFC 5155, Section 8
@@ -1250,9 +1295,9 @@ static int nsec3_hashed_domain(DnsResourceRecord *nsec3, const char *domain, con
  * name uses an NSEC3 record with the opt-out bit set. Lastly, if we are given insufficient NSEC3 records
  * to conclude anything we indicate this by returning NO_RR. */
 static int dnssec_test_nsec3(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result, bool *authenticated, uint32_t *ttl) {
-        _cleanup_free_ char *next_closer_domain = NULL, *wildcard = NULL, *wildcard_domain = NULL;
-        const char *zone, *p, *pp = NULL;
-        DnsResourceRecord *rr, *enclosure_rr, *suffix_rr, *wildcard_rr = NULL;
+        _cleanup_free_ char *next_closer_domain = NULL, *wildcard_domain = NULL;
+        const char *zone, *p, *pp = NULL, *wildcard;
+        DnsResourceRecord *rr, *enclosure_rr, *zone_rr, *wildcard_rr = NULL;
         DnsAnswerFlags flags;
         int hashed_size, r;
         bool a, no_closer = false, no_wildcard = false, optout = false;
@@ -1267,14 +1312,14 @@ static int dnssec_test_nsec3(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecR
          * parameters. */
         zone = DNS_RESOURCE_KEY_NAME(key);
         for (;;) {
-                DNS_ANSWER_FOREACH_FLAGS(suffix_rr, flags, answer) {
-                        r = nsec3_is_good(suffix_rr, flags, NULL);
+                DNS_ANSWER_FOREACH_FLAGS(zone_rr, flags, answer) {
+                        r = nsec3_is_good(zone_rr, NULL);
                         if (r < 0)
                                 return r;
                         if (r == 0)
                                 continue;
 
-                        r = dns_name_equal_skip(DNS_RESOURCE_KEY_NAME(suffix_rr->key), 1, zone);
+                        r = dns_name_equal_skip(DNS_RESOURCE_KEY_NAME(zone_rr->key), 1, zone);
                         if (r < 0)
                                 return r;
                         if (r > 0)
@@ -1298,7 +1343,7 @@ found_zone:
         for (;;) {
                 _cleanup_free_ char *hashed_domain = NULL;
 
-                hashed_size = nsec3_hashed_domain(suffix_rr, p, zone, &hashed_domain);
+                hashed_size = nsec3_hashed_domain_make(zone_rr, p, zone, &hashed_domain);
                 if (hashed_size == -EOPNOTSUPP) {
                         *result = DNSSEC_NSEC_UNSUPPORTED_ALGORITHM;
                         return 0;
@@ -1308,7 +1353,7 @@ found_zone:
 
                 DNS_ANSWER_FOREACH_FLAGS(enclosure_rr, flags, answer) {
 
-                        r = nsec3_is_good(enclosure_rr, flags, suffix_rr);
+                        r = nsec3_is_good(enclosure_rr, zone_rr);
                         if (r < 0)
                                 return r;
                         if (r == 0)
@@ -1377,38 +1422,31 @@ found_closest_encloser:
 
         /* Prove that there is no next closer and whether or not there is a wildcard domain. */
 
-        wildcard = strappend("*.", p);
-        if (!wildcard)
-                return -ENOMEM;
-
-        r = nsec3_hashed_domain(enclosure_rr, wildcard, zone, &wildcard_domain);
+        wildcard = strjoina("*.", p);
+        r = nsec3_hashed_domain_make(enclosure_rr, wildcard, zone, &wildcard_domain);
         if (r < 0)
                 return r;
         if (r != hashed_size)
                 return -EBADMSG;
 
-        r = nsec3_hashed_domain(enclosure_rr, pp, zone, &next_closer_domain);
+        r = nsec3_hashed_domain_make(enclosure_rr, pp, zone, &next_closer_domain);
         if (r < 0)
                 return r;
         if (r != hashed_size)
                 return -EBADMSG;
 
         DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) {
-                _cleanup_free_ char *label = NULL, *next_hashed_domain = NULL;
+                _cleanup_free_ char *next_hashed_domain = NULL;
 
-                r = nsec3_is_good(rr, flags, suffix_rr);
+                r = nsec3_is_good(rr, zone_rr);
                 if (r < 0)
                         return r;
                 if (r == 0)
                         continue;
 
-                label = base32hexmem(rr->nsec3.next_hashed_name, rr->nsec3.next_hashed_name_size, false);
-                if (!label)
-                        return -ENOMEM;
-
-                next_hashed_domain = strjoin(label, ".", zone, NULL);
-                if (!next_hashed_domain)
-                        return -ENOMEM;
+                r = nsec3_hashed_domain_format(rr->nsec3.next_hashed_name, rr->nsec3.next_hashed_name_size, zone, &next_hashed_domain);
+                if (r < 0)
+                        return r;
 
                 r = dns_name_between(DNS_RESOURCE_KEY_NAME(rr->key), next_closer_domain, next_hashed_domain);
                 if (r < 0)
@@ -1497,7 +1535,7 @@ found_closest_encloser:
         return 0;
 }
 
-int dnssec_test_nsec(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result, bool *authenticated, uint32_t *ttl) {
+int dnssec_nsec_test(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result, bool *authenticated, uint32_t *ttl) {
         DnsResourceRecord *rr;
         bool have_nsec3 = false;
         DnsAnswerFlags flags;
@@ -1566,15 +1604,90 @@ int dnssec_test_nsec(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *r
         return 0;
 }
 
-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);
+int dnssec_nsec_test_between(DnsAnswer *answer, const char *name, const char *zone, bool *authenticated) {
+        DnsResourceRecord *rr;
+        DnsAnswerFlags flags;
+        int r;
+
+        assert(name);
+        assert(zone);
+
+        /* Checks whether there's an NSEC/NSEC3 that proves that the specified 'name' is non-existing in the specified
+         * 'zone'. The 'zone' must be a suffix of the 'name'. */
+
+        DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) {
+                bool found = false;
+
+                r = dns_name_endswith(DNS_RESOURCE_KEY_NAME(rr->key), zone);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        continue;
+
+                switch (rr->key->type) {
+
+                case DNS_TYPE_NSEC:
+                        r = dns_name_between(DNS_RESOURCE_KEY_NAME(rr->key), name, rr->nsec.next_domain_name);
+                        if (r < 0)
+                                return r;
+
+                        found = r > 0;
+                        break;
+
+                case DNS_TYPE_NSEC3: {
+                        _cleanup_free_ char *hashed_domain = NULL, *next_hashed_domain = NULL;
+
+                        r = nsec3_is_good(rr, NULL);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                break;
+
+                        /* Format the domain we are testing with the NSEC3 RR's hash function */
+                        r = nsec3_hashed_domain_make(
+                                        rr,
+                                        name,
+                                        zone,
+                                        &hashed_domain);
+                        if (r < 0)
+                                return r;
+                        if ((size_t) r != rr->nsec3.next_hashed_name_size)
+                                break;
+
+                        /* Format the NSEC3's next hashed name as proper domain name */
+                        r = nsec3_hashed_domain_format(
+                                        rr->nsec3.next_hashed_name,
+                                        rr->nsec3.next_hashed_name_size,
+                                        zone,
+                                        &next_hashed_domain);
+                        if (r < 0)
+                                return r;
+
+                        r = dns_name_between(DNS_RESOURCE_KEY_NAME(rr->key), hashed_domain, next_hashed_domain);
+                        if (r < 0)
+                                return r;
+
+                        found = r > 0;
+                        break;
+                }
+
+                default:
+                        continue;
+                }
+
+                if (found) {
+                        if (authenticated)
+                                *authenticated = flags & DNS_ANSWER_AUTHENTICATED;
+                        return 1;
+                }
+        }
+
+        return 0;
+}
 
 static const char* const dnssec_result_table[_DNSSEC_RESULT_MAX] = {
         [DNSSEC_VALIDATED] = "validated",
+        [DNSSEC_VALIDATED_WILDCARD] = "validated-wildcard",
         [DNSSEC_INVALID] = "invalid",
         [DNSSEC_SIGNATURE_EXPIRED] = "signature-expired",
         [DNSSEC_UNSUPPORTED_ALGORITHM] = "unsupported-algorithm",