]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
resolved: add basic proof of non-existance support for NSEC+NSEC3
authorLennart Poettering <lennart@poettering.net>
Mon, 14 Dec 2015 20:26:42 +0000 (21:26 +0100)
committerLennart Poettering <lennart@poettering.net>
Mon, 14 Dec 2015 20:28:39 +0000 (21:28 +0100)
Note that this is not complete yet, as we don't handle wildcard domains
correctly, nor handle domains correctly that use empty non-terminals.

src/resolve/resolved-dns-answer.c
src/resolve/resolved-dns-dnssec.c
src/resolve/resolved-dns-dnssec.h
src/resolve/resolved-dns-transaction.c
src/resolve/test-dnssec.c

index 6a37345e7e0a75b17369f8d570884c5da952b4ef..5355303bd33aca9fa91ede7d3b14b4732458822c 100644 (file)
@@ -22,6 +22,7 @@
 #include "alloc-util.h"
 #include "dns-domain.h"
 #include "resolved-dns-answer.h"
+#include "resolved-dns-dnssec.h"
 #include "string-util.h"
 
 DnsAnswer *dns_answer_new(unsigned n) {
index 9eb54d44db12481314c4cfe17351510ab684957f..d7d50c164cd7ee127a360b6bdb7084439c99ce7d 100644 (file)
@@ -23,6 +23,7 @@
 
 #include "alloc-util.h"
 #include "dns-domain.h"
+#include "hexdecoct.h"
 #include "resolved-dns-dnssec.h"
 #include "resolved-dns-packet.h"
 #include "string-table.h"
@@ -795,6 +796,187 @@ int dnssec_verify_dnskey_search(DnsResourceRecord *dnskey, DnsAnswer *validated_
         return 0;
 }
 
+int dnssec_nsec3_hash(DnsResourceRecord *nsec3, const char *name, void *ret) {
+        uint8_t wire_format[DNS_WIRE_FOMAT_HOSTNAME_MAX];
+        gcry_md_hd_t md = NULL;
+        size_t hash_size;
+        int algorithm;
+        void *result;
+        unsigned k;
+        int r;
+
+        assert(nsec3);
+        assert(name);
+        assert(ret);
+
+        if (nsec3->key->type != DNS_TYPE_NSEC3)
+                return -EINVAL;
+
+        algorithm = digest_to_gcrypt(nsec3->nsec3.algorithm);
+        if (algorithm < 0)
+                return algorithm;
+
+        initialize_libgcrypt();
+
+        hash_size = gcry_md_get_algo_dlen(algorithm);
+        assert(hash_size > 0);
+
+        if (nsec3->nsec3.next_hashed_name_size != hash_size)
+                return -EINVAL;
+
+        r = dns_name_to_wire_format(name, wire_format, sizeof(wire_format), true);
+        if (r < 0)
+                return r;
+
+        gcry_md_open(&md, algorithm, 0);
+        if (!md)
+                return -EIO;
+
+        gcry_md_write(md, wire_format, r);
+        gcry_md_write(md, nsec3->nsec3.salt, nsec3->nsec3.salt_size);
+
+        result = gcry_md_read(md, 0);
+        if (!result) {
+                r = -EIO;
+                goto finish;
+        }
+
+        for (k = 0; k < nsec3->nsec3.iterations; k++) {
+                uint8_t tmp[hash_size];
+                memcpy(tmp, result, hash_size);
+
+                gcry_md_reset(md);
+                gcry_md_write(md, tmp, hash_size);
+                gcry_md_write(md, nsec3->nsec3.salt, nsec3->nsec3.salt_size);
+
+                result = gcry_md_read(md, 0);
+                if (!result) {
+                        r = -EIO;
+                        goto finish;
+                }
+        }
+
+        memcpy(ret, result, hash_size);
+        r = (int) hash_size;
+
+finish:
+        gcry_md_close(md);
+        return r;
+}
+
+int dnssec_test_nsec(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result) {
+        DnsResourceRecord *rr;
+        int r;
+
+        assert(key);
+        assert(result);
+
+        /* Look for any NSEC/NSEC3 RRs that say something about the specified key. */
+
+        DNS_ANSWER_FOREACH(rr, answer) {
+
+                if (rr->key->class != key->class)
+                        continue;
+
+                switch (rr->key->type) {
+
+                case DNS_TYPE_NSEC:
+
+                        r = dns_name_equal(DNS_RESOURCE_KEY_NAME(rr->key), DNS_RESOURCE_KEY_NAME(key));
+                        if (r < 0)
+                                return r;
+                        if (r > 0) {
+                                *result = bitmap_isset(rr->nsec.types, key->type) ? DNSSEC_NSEC_FOUND : DNSSEC_NSEC_NODATA;
+                                return 0;
+                        }
+
+                        r = dns_name_between(DNS_RESOURCE_KEY_NAME(rr->key), DNS_RESOURCE_KEY_NAME(key), rr->nsec.next_domain_name);
+                        if (r < 0)
+                                return r;
+                        if (r > 0) {
+                                *result = DNSSEC_NSEC_NXDOMAIN;
+                                return 0;
+                        }
+                        break;
+
+                case DNS_TYPE_NSEC3: {
+                        _cleanup_free_ void *decoded = NULL;
+                        size_t decoded_size;
+                        char label[DNS_LABEL_MAX];
+                        uint8_t hashed[DNSSEC_HASH_SIZE_MAX];
+                        int label_length, c, q;
+                        const char *p;
+                        bool covered;
+
+                        /* 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;
+
+                        p = DNS_RESOURCE_KEY_NAME(rr->key);
+                        label_length = dns_label_unescape(&p, label, sizeof(label));
+                        if (label_length < 0)
+                                return label_length;
+                        if (label_length == 0)
+                                continue;
+
+                        r = dns_name_endswith(DNS_RESOURCE_KEY_NAME(key), p);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                continue;
+
+                        r = unbase32hexmem(label, label_length, false, &decoded, &decoded_size);
+                        if (r == -EINVAL)
+                                continue;
+                        if (r < 0)
+                                return r;
+
+                        if (decoded_size != rr->nsec3.next_hashed_name_size)
+                                continue;
+
+                        c = memcmp(decoded, rr->nsec3.next_hashed_name, decoded_size);
+                        if (c == 0)
+                                continue;
+
+                        r = dnssec_nsec3_hash(rr, DNS_RESOURCE_KEY_NAME(key), hashed);
+                        /* RFC 5155, Section 8.1 says we MUST ignore NSEC3 RRs with unknown algorithms */
+                        if (r == -EOPNOTSUPP)
+                                continue;
+                        if (r < 0)
+                                return r;
+                        if ((size_t) r != decoded_size)
+                                continue;
+
+                        r = memcmp(decoded, hashed, decoded_size);
+                        if (r == 0) {
+                                *result = bitmap_isset(rr->nsec3.types, key->type) ? DNSSEC_NSEC_FOUND : DNSSEC_NSEC_NODATA;
+                                return 0;
+                        }
+
+                        q = memcmp(hashed, rr->nsec3.next_hashed_name, decoded_size);
+
+                        covered = c < 0 ?
+                                r < 0 && q < 0 :
+                                q < 0 || r < 0;
+
+                        if (covered) {
+                                *result = DNSSEC_NSEC_NXDOMAIN;
+                                return 0;
+                        }
+
+                        break;
+                }
+
+                default:
+                        break;
+                }
+        }
+
+        /* No approproate NSEC RR found, report this. */
+        *result = DNSSEC_NSEC_NO_RR;
+        return 0;
+}
+
 static const char* const dnssec_mode_table[_DNSSEC_MODE_MAX] = {
         [DNSSEC_NO] = "no",
         [DNSSEC_TRUST] = "trust",
@@ -811,5 +993,6 @@ static const char* const dnssec_result_table[_DNSSEC_RESULT_MAX] = {
         [DNSSEC_MISSING_KEY] = "missing-key",
         [DNSSEC_UNSIGNED] = "unsigned",
         [DNSSEC_FAILED_AUXILIARY] = "failed-auxiliary",
+        [DNSSEC_NSEC_MISMATCH] = "nsec-mismatch",
 };
 DEFINE_STRING_TABLE_LOOKUP(dnssec_result, DnssecResult);
index f33abe3e11a81c92da6c2171853c87585b24e1c3..442e3013027be9e607f712c854e98a13e8409713 100644 (file)
@@ -56,12 +56,16 @@ enum DnssecResult {
         /* These two are added by the DnsTransaction logic */
         DNSSEC_UNSIGNED,
         DNSSEC_FAILED_AUXILIARY,
+        DNSSEC_NSEC_MISMATCH,
         _DNSSEC_RESULT_MAX,
         _DNSSEC_RESULT_INVALID = -1
 };
 
 #define DNSSEC_CANONICAL_HOSTNAME_MAX (DNS_HOSTNAME_MAX + 2)
 
+/* The longest digest we'll ever generate, of all digest algorithms we support */
+#define DNSSEC_HASH_SIZE_MAX (MAX(20, 32))
+
 int dnssec_rrsig_match_dnskey(DnsResourceRecord *rrsig, DnsResourceRecord *dnskey);
 int dnssec_key_match_rrsig(DnsResourceKey *key, DnsResourceRecord *rrsig);
 
@@ -75,6 +79,17 @@ uint16_t dnssec_keytag(DnsResourceRecord *dnskey);
 
 int dnssec_canonicalize(const char *n, char *buffer, size_t buffer_max);
 
+int dnssec_nsec3_hash(DnsResourceRecord *nsec3, const char *name, void *ret);
+
+typedef enum DnssecNsecResult {
+        DNSSEC_NSEC_NO_RR,     /* No suitable NSEC/NSEC3 RR found */
+        DNSSEC_NSEC_NXDOMAIN,
+        DNSSEC_NSEC_NODATA,
+        DNSSEC_NSEC_FOUND,
+} DnssecNsecResult;
+
+int dnssec_test_nsec(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result);
+
 const char* dnssec_mode_to_string(DnssecMode m) _const_;
 DnssecMode dnssec_mode_from_string(const char *s) _pure_;
 
index 045627340b6528f77f369fe2c3069295cbe1f553..f09788b0c67159da650d54b9ef11a66ab1e5dc06 100644 (file)
@@ -1472,8 +1472,57 @@ int dns_transaction_validate_dnssec(DnsTransaction *t) {
         /* Everything that's now in t->answer is known to be good, hence cacheable. */
         t->n_answer_cacheable = (unsigned) -1; /* everything! */
 
-        t->answer_authenticated = true;
-        t->dnssec_result = DNSSEC_VALIDATED;
+        /* At this point the answer only contains validated
+         * RRsets. Now, let's see if it actually answers the question
+         * we asked. If so, great! If it doesn't, then see if
+         * NSEC/NSEC3 can prove this. */
+        r = dns_answer_match_key(t->answer, t->key);
+        if (r < 0)
+                return r;
+        if (r > 0) {
+                /* Yes, it answer the question, everything is authenticated. */
+                t->dnssec_result = DNSSEC_VALIDATED;
+                t->answer_rcode = DNS_RCODE_SUCCESS;
+                t->answer_authenticated = true;
+        } else if (r == 0) {
+                DnssecNsecResult nr;
+
+                /* Bummer! Let's check NSEC/NSEC3 */
+                r = dnssec_test_nsec(t->answer, t->key, &nr);
+                if (r < 0)
+                        return r;
+
+                switch (nr) {
+
+                case DNSSEC_NSEC_NXDOMAIN:
+                        /* NSEC proves the domain doesn't exist. Very good. */
+                        t->dnssec_result = DNSSEC_VALIDATED;
+                        t->answer_rcode = DNS_RCODE_NXDOMAIN;
+                        t->answer_authenticated = true;
+                        break;
+
+                case DNSSEC_NSEC_NODATA:
+                        /* NSEC proves that there's no data here, very good. */
+                        t->dnssec_result = DNSSEC_VALIDATED;
+                        t->answer_rcode = DNS_RCODE_SUCCESS;
+                        t->answer_authenticated = true;
+                        break;
+
+                case DNSSEC_NSEC_NO_RR:
+                        /* No NSEC data? Bummer! */
+                        t->dnssec_result = DNSSEC_UNSIGNED;
+                        break;
+
+                case DNSSEC_NSEC_FOUND:
+                        /* NSEC says it needs to be there, but we couldn't find it? Bummer! */
+                        t->dnssec_result = DNSSEC_NSEC_MISMATCH;
+                        break;
+
+                default:
+                        assert_not_reached("Unexpected NSEC result.");
+                }
+        }
+
         return 1;
 }
 
index cbcb0fd94f02b2bd1226a426305356165e692d98..807eeb3d9a6f15b96853012c38a8f2e9743fea60 100644 (file)
@@ -27,6 +27,7 @@
 #include "resolved-dns-dnssec.h"
 #include "resolved-dns-rr.h"
 #include "string-util.h"
+#include "hexdecoct.h"
 
 static void test_dnssec_verify_rrset2(void) {
 
@@ -305,12 +306,46 @@ static void test_dnssec_canonicalize(void) {
         test_dnssec_canonicalize_one("FOO..bar.", NULL, -EINVAL);
 }
 
+static void test_dnssec_nsec3_hash(void) {
+        static const uint8_t salt[] = { 0xB0, 0x1D, 0xFA, 0xCE };
+        static const uint8_t next_hashed_name[] = { 0x84, 0x10, 0x26, 0x53, 0xc9, 0xfa, 0x4d, 0x85, 0x6c, 0x97, 0x82, 0xe2, 0x8f, 0xdf, 0x2d, 0x5e, 0x87, 0x69, 0xc4, 0x52 };
+        _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+        _cleanup_free_ char *a = NULL, *b = NULL;
+        uint8_t h[DNSSEC_HASH_SIZE_MAX];
+        int k;
+
+        /* The NSEC3 RR for eurid.eu on 2015-12-14. */
+        rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_NSEC3, "PJ8S08RR45VIQDAQGE7EN3VHKNROTBMM.eurid.eu.");
+        assert_se(rr);
+
+        rr->nsec3.algorithm = DNSSEC_DIGEST_SHA1;
+        rr->nsec3.flags = 1;
+        rr->nsec3.iterations = 1;
+        rr->nsec3.salt = memdup(salt, sizeof(salt));
+        assert_se(rr->nsec3.salt);
+        rr->nsec3.salt_size = sizeof(salt);
+        rr->nsec3.next_hashed_name = memdup(next_hashed_name, sizeof(next_hashed_name));
+        assert_se(rr->nsec3.next_hashed_name);
+        rr->nsec3.next_hashed_name_size = sizeof(next_hashed_name);
+
+        assert_se(dns_resource_record_to_string(rr, &a) >= 0);
+        log_info("NSEC3: %s", a);
+
+        k = dnssec_nsec3_hash(rr, "eurid.eu", &h);
+        assert_se(k >= 0);
+
+        b = base32hexmem(h, k, false);
+        assert_se(b);
+        assert_se(strcasecmp(b, "PJ8S08RR45VIQDAQGE7EN3VHKNROTBMM") == 0);
+}
+
 int main(int argc, char*argv[]) {
 
         test_dnssec_canonicalize();
         test_dnssec_verify_dns_key();
         test_dnssec_verify_rrset();
         test_dnssec_verify_rrset2();
+        test_dnssec_nsec3_hash();
 
         return 0;
 }