diff options
author | Tom Gundersen <teg@jklm.no> | 2015-12-03 01:52:01 +0100 |
---|---|---|
committer | Tom Gundersen <teg@jklm.no> | 2015-12-03 01:52:01 +0100 |
commit | 3f967a4ccf1846514bd638f38e4f487bb4e1c317 (patch) | |
tree | 07bb4f6a9b1defea1df208a2f5097433a2e9787e /src/resolve | |
parent | 5449f1e3312e5467ce321fc2d7aa16a0ce0c60d2 (diff) | |
parent | 964ef14c2525f3a0311acb24c6814c5bfbe43cfc (diff) |
Merge pull request #2087 from poettering/dnssec
Basic DNSSEC support, and unrelated fixes
Diffstat (limited to 'src/resolve')
-rw-r--r-- | src/resolve/resolved-bus.c | 12 | ||||
-rw-r--r-- | src/resolve/resolved-dns-answer.h | 25 | ||||
-rw-r--r-- | src/resolve/resolved-dns-cache.c | 45 | ||||
-rw-r--r-- | src/resolve/resolved-dns-cache.h | 1 | ||||
-rw-r--r-- | src/resolve/resolved-dns-dnssec.c | 692 | ||||
-rw-r--r-- | src/resolve/resolved-dns-dnssec.h | 49 | ||||
-rw-r--r-- | src/resolve/resolved-dns-packet.c | 106 | ||||
-rw-r--r-- | src/resolve/resolved-dns-packet.h | 34 | ||||
-rw-r--r-- | src/resolve/resolved-dns-question.h | 13 | ||||
-rw-r--r-- | src/resolve/resolved-dns-rr.c | 78 | ||||
-rw-r--r-- | src/resolve/resolved-dns-rr.h | 49 | ||||
-rw-r--r-- | src/resolve/resolved-dns-scope.c | 6 | ||||
-rw-r--r-- | src/resolve/test-dnssec.c | 217 |
13 files changed, 1185 insertions, 142 deletions
diff --git a/src/resolve/resolved-bus.c b/src/resolve/resolved-bus.c index f86c4ceb05..ddde3af0c3 100644 --- a/src/resolve/resolved-bus.c +++ b/src/resolve/resolved-bus.c @@ -425,8 +425,6 @@ fail: } static int bus_message_append_rr(sd_bus_message *m, DnsResourceRecord *rr, int ifindex) { - _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; - size_t start; int r; assert(m); @@ -443,17 +441,11 @@ static int bus_message_append_rr(sd_bus_message *m, DnsResourceRecord *rr, int i if (r < 0) return r; - r = dns_packet_new(&p, DNS_PROTOCOL_DNS, 0); + r = dns_resource_record_to_wire_format(rr, false); if (r < 0) return r; - p->refuse_compression = true; - - r = dns_packet_append_rr(p, rr, &start); - if (r < 0) - return r; - - r = sd_bus_message_append_array(m, 'y', DNS_PACKET_DATA(p) + start, p->size - start); + r = sd_bus_message_append_array(m, 'y', rr->wire_format, rr->wire_format_size); if (r < 0) return r; diff --git a/src/resolve/resolved-dns-answer.h b/src/resolve/resolved-dns-answer.h index 8814919deb..89c254b02e 100644 --- a/src/resolve/resolved-dns-answer.h +++ b/src/resolve/resolved-dns-answer.h @@ -24,6 +24,7 @@ typedef struct DnsAnswer DnsAnswer; typedef struct DnsAnswerItem DnsAnswerItem; +#include "macro.h" #include "resolved-dns-rr.h" /* A simple array of resource records. We keep track of the @@ -59,19 +60,23 @@ int dns_answer_reserve(DnsAnswer **a, unsigned n_free); DEFINE_TRIVIAL_CLEANUP_FUNC(DnsAnswer*, dns_answer_unref); -#define DNS_ANSWER_FOREACH(kk, a) \ - for (unsigned _i = ({ \ +#define _DNS_ANSWER_FOREACH(q, kk, a) \ + for (unsigned UNIQ_T(i, q) = ({ \ (kk) = ((a) && (a)->n_rrs > 0) ? (a)->items[0].rr : NULL; \ 0; \ - }); \ - (a) && ((_i) < (a)->n_rrs); \ - _i++, (kk) = (_i < (a)->n_rrs ? (a)->items[_i].rr : NULL)) + }); \ + (a) && (UNIQ_T(i, q) < (a)->n_rrs); \ + UNIQ_T(i, q)++, (kk) = (UNIQ_T(i, q) < (a)->n_rrs ? (a)->items[UNIQ_T(i, q)].rr : NULL)) -#define DNS_ANSWER_FOREACH_IFINDEX(kk, ifindex, a) \ - for (unsigned _i = ({ \ +#define DNS_ANSWER_FOREACH(kk, a) _DNS_ANSWER_FOREACH(UNIQ, kk, a) + +#define _DNS_ANSWER_FOREACH_IFINDEX(q, kk, ifindex, a) \ + for (unsigned UNIQ_T(i, q) = ({ \ (kk) = ((a) && (a)->n_rrs > 0) ? (a)->items[0].rr : NULL; \ (ifindex) = ((a) && (a)->n_rrs > 0) ? (a)->items[0].ifindex : 0; \ 0; \ - }); \ - (a) && ((_i) < (a)->n_rrs); \ - _i++, (kk) = ((_i < (a)->n_rrs) ? (a)->items[_i].rr : NULL), (ifindex) = ((_i < (a)->n_rrs) ? (a)->items[_i].ifindex : 0)) + }); \ + (a) && (UNIQ_T(i, q) < (a)->n_rrs); \ + UNIQ_T(i, q)++, (kk) = ((UNIQ_T(i, q) < (a)->n_rrs) ? (a)->items[UNIQ_T(i, q)].rr : NULL), (ifindex) = ((UNIQ_T(i, q) < (a)->n_rrs) ? (a)->items[UNIQ_T(i, q)].ifindex : 0)) + +#define DNS_ANSWER_FOREACH_IFINDEX(kk, ifindex, a) _DNS_ANSWER_FOREACH_IFINDEX(UNIQ, kk, ifindex, a) diff --git a/src/resolve/resolved-dns-cache.c b/src/resolve/resolved-dns-cache.c index d963ce6e00..3f34017789 100644 --- a/src/resolve/resolved-dns-cache.c +++ b/src/resolve/resolved-dns-cache.c @@ -521,8 +521,8 @@ fail: return r; } -static DnsCacheItem *dns_cache_get_by_key_follow_cname(DnsCache *c, DnsResourceKey *k) { - _cleanup_(dns_resource_key_unrefp) DnsResourceKey *cname_key = NULL; +static DnsCacheItem *dns_cache_get_by_key_follow_cname_dname_nsec(DnsCache *c, DnsResourceKey *k) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *nsec_key = NULL, *cname_key = NULL; DnsCacheItem *i; const char *n; int r; @@ -534,20 +534,29 @@ static DnsCacheItem *dns_cache_get_by_key_follow_cname(DnsCache *c, DnsResourceK * much, after all this is just a cache */ i = hashmap_get(c->by_key, k); - if (i || k->type == DNS_TYPE_CNAME || k->type == DNS_TYPE_DNAME) + if (i || IN_SET(k->type, DNS_TYPE_CNAME, DNS_TYPE_DNAME, DNS_TYPE_NSEC)) + return i; + + n = DNS_RESOURCE_KEY_NAME(k); + + /* Check if we have an NSEC record instead for the name. */ + nsec_key = dns_resource_key_new(k->class, DNS_TYPE_NSEC, n); + if (!nsec_key) + return NULL; + + i = hashmap_get(c->by_key, nsec_key); + if (i) return i; /* Check if we have a CNAME record instead */ cname_key = dns_resource_key_new_cname(k); if (!cname_key) return NULL; - i = hashmap_get(c->by_key, cname_key); if (i) return i; /* OK, let's look for cached DNAME records. */ - n = DNS_RESOURCE_KEY_NAME(k); for (;;) { _cleanup_(dns_resource_key_unrefp) DnsResourceKey *dname_key = NULL; char label[DNS_LABEL_MAX]; @@ -578,6 +587,7 @@ int dns_cache_lookup(DnsCache *c, DnsResourceKey *key, int *rcode, DnsAnswer **r int r; bool nxdomain = false; _cleanup_free_ char *key_str = NULL; + DnsResourceRecord *nsec = NULL; DnsCacheItem *j, *first; assert(c); @@ -601,7 +611,7 @@ int dns_cache_lookup(DnsCache *c, DnsResourceKey *key, int *rcode, DnsAnswer **r return 0; } - first = dns_cache_get_by_key_follow_cname(c, key); + first = dns_cache_get_by_key_follow_cname_dname_nsec(c, key); if (!first) { /* If one question cannot be answered we need to refresh */ @@ -617,9 +627,11 @@ int dns_cache_lookup(DnsCache *c, DnsResourceKey *key, int *rcode, DnsAnswer **r } LIST_FOREACH(by_key, j, first) { - if (j->rr) + if (j->rr) { + if (j->rr->key->type == DNS_TYPE_NSEC) + nsec = j->rr; n++; - else if (j->type == DNS_CACHE_NXDOMAIN) + } else if (j->type == DNS_CACHE_NXDOMAIN) nxdomain = true; } @@ -627,9 +639,22 @@ int dns_cache_lookup(DnsCache *c, DnsResourceKey *key, int *rcode, DnsAnswer **r if (r < 0) return r; + if (nsec && key->type != DNS_TYPE_NSEC) { + log_debug("NSEC NODATA cache hit for %s", key_str); + + /* We only found an NSEC record that matches our name. + * If it says the type doesn't exit report + * NODATA. Otherwise report a cache miss. */ + + *ret = NULL; + *rcode = DNS_RCODE_SUCCESS; + + return !bitmap_isset(nsec->nsec.types, key->type); + } + log_debug("%s cache hit for %s", - nxdomain ? "NXDOMAIN" : - n > 0 ? "Positive" : "NODATA", + n > 0 ? "Positive" : + nxdomain ? "NXDOMAIN" : "NODATA", key_str); if (n <= 0) { diff --git a/src/resolve/resolved-dns-cache.h b/src/resolve/resolved-dns-cache.h index 164435b4fb..561d31ad99 100644 --- a/src/resolve/resolved-dns-cache.h +++ b/src/resolve/resolved-dns-cache.h @@ -21,7 +21,6 @@ along with systemd; If not, see <http://www.gnu.org/licenses/>. ***/ - #include "hashmap.h" #include "list.h" #include "prioq.h" diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c new file mode 100644 index 0000000000..a32e938045 --- /dev/null +++ b/src/resolve/resolved-dns-dnssec.c @@ -0,0 +1,692 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2015 Lennart Poettering + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see <http://www.gnu.org/licenses/>. +***/ + +#include <gcrypt.h> + +#include "alloc-util.h" +#include "dns-domain.h" +#include "resolved-dns-dnssec.h" +#include "resolved-dns-packet.h" + +/* Open question: + * + * How does the DNSSEC canonical form of a hostname with a label + * containing a dot look like, the way DNS-SD does it? + * + * */ + +#define VERIFY_RRS_MAX 256 +#define MAX_KEY_SIZE (32*1024) + +/* + * The DNSSEC Chain of trust: + * + * Normal RRs are protected via RRSIG RRs in combination with DNSKEY RRs, all in the same zone + * DNSKEY RRs are either protected like normal RRs, or via a DS from a zone "higher" up the tree + * DS RRs are protected like normal RRs + * + * Example chain: + * Normal RR → RRSIG/DNSKEY+ → DS → RRSIG/DNSKEY+ → DS → ... → DS → RRSIG/DNSKEY+ → DS + */ + +static bool dnssec_algorithm_supported(int algorithm) { + return IN_SET(algorithm, + DNSSEC_ALGORITHM_RSASHA1, + DNSSEC_ALGORITHM_RSASHA1_NSEC3_SHA1, + DNSSEC_ALGORITHM_RSASHA256, + DNSSEC_ALGORITHM_RSASHA512); +} + +static bool dnssec_digest_supported(int digest) { + return IN_SET(digest, + DNSSEC_DIGEST_SHA1, + DNSSEC_DIGEST_SHA256); +} + +uint16_t dnssec_keytag(DnsResourceRecord *dnskey) { + const uint8_t *p; + uint32_t sum; + size_t i; + + /* The algorithm from RFC 4034, Appendix B. */ + + assert(dnskey); + assert(dnskey->key->type == DNS_TYPE_DNSKEY); + + sum = (uint32_t) dnskey->dnskey.flags + + ((((uint32_t) dnskey->dnskey.protocol) << 8) + (uint32_t) dnskey->dnskey.algorithm); + + p = dnskey->dnskey.key; + + for (i = 0; i < dnskey->dnskey.key_size; i++) + sum += (i & 1) == 0 ? (uint32_t) p[i] << 8 : (uint32_t) p[i]; + + sum += (sum >> 16) & UINT32_C(0xFFFF); + + return sum & UINT32_C(0xFFFF); +} + +static int rr_compare(const void *a, const void *b) { + DnsResourceRecord **x = (DnsResourceRecord**) a, **y = (DnsResourceRecord**) b; + size_t m; + int r; + + /* Let's order the RRs according to RFC 4034, Section 6.3 */ + + assert(x); + assert(*x); + assert((*x)->wire_format); + assert(y); + assert(*y); + assert((*y)->wire_format); + + m = MIN((*x)->wire_format_size, (*y)->wire_format_size); + + r = memcmp((*x)->wire_format, (*y)->wire_format, m); + if (r != 0) + return r; + + if ((*x)->wire_format_size < (*y)->wire_format_size) + return -1; + else if ((*x)->wire_format_size > (*y)->wire_format_size) + return 1; + + return 0; +} + +static int dnssec_rsa_verify( + const char *hash_algorithm, + const void *signature, size_t signature_size, + const void *data, size_t data_size, + const void *exponent, size_t exponent_size, + const void *modulus, size_t modulus_size) { + + gcry_sexp_t public_key_sexp = NULL, data_sexp = NULL, signature_sexp = NULL; + gcry_mpi_t n = NULL, e = NULL, s = NULL; + gcry_error_t ge; + int r; + + assert(hash_algorithm); + + ge = gcry_mpi_scan(&s, GCRYMPI_FMT_USG, signature, signature_size, NULL); + if (ge != 0) { + r = -EIO; + goto finish; + } + + ge = gcry_mpi_scan(&e, GCRYMPI_FMT_USG, exponent, exponent_size, NULL); + if (ge != 0) { + r = -EIO; + goto finish; + } + + ge = gcry_mpi_scan(&n, GCRYMPI_FMT_USG, modulus, modulus_size, NULL); + if (ge != 0) { + r = -EIO; + goto finish; + } + + ge = gcry_sexp_build(&signature_sexp, + NULL, + "(sig-val (rsa (s %m)))", + s); + + if (ge != 0) { + r = -EIO; + goto finish; + } + + ge = gcry_sexp_build(&data_sexp, + NULL, + "(data (flags pkcs1) (hash %s %b))", + hash_algorithm, + (int) data_size, + data); + if (ge != 0) { + r = -EIO; + goto finish; + } + + ge = gcry_sexp_build(&public_key_sexp, + NULL, + "(public-key (rsa (n %m) (e %m)))", + n, + e); + if (ge != 0) { + r = -EIO; + goto finish; + } + + ge = gcry_pk_verify(signature_sexp, data_sexp, public_key_sexp); + if (ge == GPG_ERR_BAD_SIGNATURE) + r = 0; + else if (ge != 0) + r = -EIO; + else + r = 1; + +finish: + if (e) + gcry_mpi_release(e); + if (n) + gcry_mpi_release(n); + if (s) + gcry_mpi_release(s); + + if (public_key_sexp) + gcry_sexp_release(public_key_sexp); + if (signature_sexp) + gcry_sexp_release(signature_sexp); + if (data_sexp) + gcry_sexp_release(data_sexp); + + return r; +} + +static void md_add_uint8(gcry_md_hd_t md, uint8_t v) { + gcry_md_write(md, &v, sizeof(v)); +} + +static void md_add_uint16(gcry_md_hd_t md, uint16_t v) { + v = htobe16(v); + gcry_md_write(md, &v, sizeof(v)); +} + +static void md_add_uint32(gcry_md_hd_t md, uint32_t v) { + v = htobe32(v); + gcry_md_write(md, &v, sizeof(v)); +} + +static int dnssec_rrsig_expired(DnsResourceRecord *rrsig, usec_t realtime) { + usec_t expiration, inception, skew; + + assert(rrsig); + assert(rrsig->key->type == DNS_TYPE_RRSIG); + + if (realtime == USEC_INFINITY) + realtime = now(CLOCK_REALTIME); + + expiration = rrsig->rrsig.expiration * USEC_PER_SEC; + inception = rrsig->rrsig.inception * USEC_PER_SEC; + + if (inception > expiration) + return -EINVAL; + + /* Permit a certain amount of clock skew of 10% of the valid time range */ + skew = (expiration - inception) / 10; + + if (inception < skew) + inception = 0; + else + inception -= skew; + + if (expiration + skew < expiration) + expiration = USEC_INFINITY; + else + expiration += skew; + + return realtime < inception || realtime > expiration; +} + +int dnssec_verify_rrset( + DnsAnswer *a, + DnsResourceKey *key, + DnsResourceRecord *rrsig, + DnsResourceRecord *dnskey, + usec_t realtime) { + + uint8_t wire_format_name[DNS_WIRE_FOMAT_HOSTNAME_MAX]; + size_t exponent_size, modulus_size, hash_size; + void *exponent, *modulus, *hash; + DnsResourceRecord **list, *rr; + gcry_md_hd_t md = NULL; + size_t k, n = 0; + int r; + + assert(key); + assert(rrsig); + assert(dnskey); + assert(rrsig->key->type == DNS_TYPE_RRSIG); + assert(dnskey->key->type == DNS_TYPE_DNSKEY); + + /* Verifies the the RRSet matching the specified "key" in "a", + * using the signature "rrsig" and the key "dnskey". It's + * assumed the RRSIG and DNSKEY match. */ + + if (!dnssec_algorithm_supported(rrsig->rrsig.algorithm)) + return -EOPNOTSUPP; + + if (a->n_rrs > VERIFY_RRS_MAX) + return -E2BIG; + + r = dnssec_rrsig_expired(rrsig, realtime); + if (r < 0) + return r; + if (r > 0) + return DNSSEC_SIGNATURE_EXPIRED; + + /* Collect all relevant RRs in a single array, so that we can look at the RRset */ + list = newa(DnsResourceRecord *, a->n_rrs); + + DNS_ANSWER_FOREACH(rr, a) { + r = dns_resource_key_equal(key, rr->key); + if (r < 0) + return r; + if (r == 0) + continue; + + /* We need the wire format for ordering, and digest calculation */ + r = dns_resource_record_to_wire_format(rr, true); + if (r < 0) + return r; + + list[n++] = rr; + } + + if (n <= 0) + return -ENODATA; + + /* Bring the RRs into canonical order */ + qsort_safe(list, n, sizeof(DnsResourceRecord), rr_compare); + + /* OK, the RRs are now in canonical order. Let's calculate the digest */ + switch (rrsig->rrsig.algorithm) { + + case DNSSEC_ALGORITHM_RSASHA1: + case DNSSEC_ALGORITHM_RSASHA1_NSEC3_SHA1: + gcry_md_open(&md, GCRY_MD_SHA1, 0); + hash_size = 20; + break; + + case DNSSEC_ALGORITHM_RSASHA256: + gcry_md_open(&md, GCRY_MD_SHA256, 0); + hash_size = 32; + break; + + case DNSSEC_ALGORITHM_RSASHA512: + gcry_md_open(&md, GCRY_MD_SHA512, 0); + hash_size = 64; + break; + + default: + assert_not_reached("Unknown digest"); + } + + if (!md) + return -EIO; + + md_add_uint16(md, rrsig->rrsig.type_covered); + md_add_uint8(md, rrsig->rrsig.algorithm); + md_add_uint8(md, rrsig->rrsig.labels); + md_add_uint32(md, rrsig->rrsig.original_ttl); + md_add_uint32(md, rrsig->rrsig.expiration); + md_add_uint32(md, rrsig->rrsig.inception); + md_add_uint16(md, rrsig->rrsig.key_tag); + + r = dns_name_to_wire_format(rrsig->rrsig.signer, wire_format_name, sizeof(wire_format_name), true); + if (r < 0) + goto finish; + gcry_md_write(md, wire_format_name, r); + + for (k = 0; k < n; k++) { + 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); + if (r < 0) + goto finish; + gcry_md_write(md, wire_format_name, r); + + md_add_uint16(md, rr->key->type); + md_add_uint16(md, rr->key->class); + md_add_uint32(md, rrsig->rrsig.original_ttl); + + assert(rr->wire_format_rdata_offset <= rr->wire_format_size); + l = rr->wire_format_size - rr->wire_format_rdata_offset; + assert(l <= 0xFFFF); + + md_add_uint16(md, (uint16_t) l); + gcry_md_write(md, (uint8_t*) rr->wire_format + rr->wire_format_rdata_offset, l); + } + + hash = gcry_md_read(md, 0); + if (!hash) { + r = -EIO; + goto finish; + } + + if (*(uint8_t*) dnskey->dnskey.key == 0) { + /* exponent is > 255 bytes long */ + + exponent = (uint8_t*) dnskey->dnskey.key + 3; + exponent_size = + ((size_t) (((uint8_t*) dnskey->dnskey.key)[0]) << 8) | + ((size_t) ((uint8_t*) dnskey->dnskey.key)[1]); + + if (exponent_size < 256) { + r = -EINVAL; + goto finish; + } + + if (3 + exponent_size >= dnskey->dnskey.key_size) { + r = -EINVAL; + goto finish; + } + + modulus = (uint8_t*) dnskey->dnskey.key + 3 + exponent_size; + modulus_size = dnskey->dnskey.key_size - 3 - exponent_size; + + } else { + /* exponent is <= 255 bytes long */ + + exponent = (uint8_t*) dnskey->dnskey.key + 1; + exponent_size = (size_t) ((uint8_t*) dnskey->dnskey.key)[0]; + + if (exponent_size <= 0) { + r = -EINVAL; + goto finish; + } + + if (1 + exponent_size >= dnskey->dnskey.key_size) { + r = -EINVAL; + goto finish; + } + + modulus = (uint8_t*) dnskey->dnskey.key + 1 + exponent_size; + modulus_size = dnskey->dnskey.key_size - 1 - exponent_size; + } + + r = dnssec_rsa_verify( + gcry_md_algo_name(gcry_md_get_algo(md)), + rrsig->rrsig.signature, rrsig->rrsig.signature_size, + hash, hash_size, + exponent, exponent_size, + modulus, modulus_size); + if (r < 0) + goto finish; + + r = r ? DNSSEC_VERIFIED : DNSSEC_INVALID; + +finish: + gcry_md_close(md); + return r; +} + +int dnssec_rrsig_match_dnskey(DnsResourceRecord *rrsig, DnsResourceRecord *dnskey) { + + assert(rrsig); + assert(dnskey); + + /* Checks if the specified DNSKEY RR matches the key used for + * the signature in the specified RRSIG RR */ + + if (rrsig->key->type != DNS_TYPE_RRSIG) + return -EINVAL; + + if (dnskey->key->type != DNS_TYPE_DNSKEY) + return 0; + if (dnskey->key->class != rrsig->key->class) + return 0; + if ((dnskey->dnskey.flags & DNSKEY_FLAG_ZONE_KEY) == 0) + return 0; + if (dnskey->dnskey.protocol != 3) + return 0; + if (dnskey->dnskey.algorithm != rrsig->rrsig.algorithm) + return 0; + + if (dnssec_keytag(dnskey) != rrsig->rrsig.key_tag) + return 0; + + return dns_name_equal(DNS_RESOURCE_KEY_NAME(dnskey->key), DNS_RESOURCE_KEY_NAME(rrsig->key)); +} + +int dnssec_key_match_rrsig(DnsResourceKey *key, DnsResourceRecord *rrsig) { + assert(key); + assert(rrsig); + + /* Checks if the specified RRSIG RR protects the RRSet of the specified RR key. */ + + if (rrsig->key->type != DNS_TYPE_RRSIG) + return 0; + if (rrsig->key->class != key->class) + return 0; + if (rrsig->rrsig.type_covered != key->type) + return 0; + + return dns_name_equal(DNS_RESOURCE_KEY_NAME(rrsig->key), DNS_RESOURCE_KEY_NAME(key)); +} + +int dnssec_verify_rrset_search( + DnsAnswer *a, + DnsResourceKey *key, + DnsAnswer *validated_dnskeys, + usec_t realtime) { + + bool found_rrsig = false, found_dnskey = false; + DnsResourceRecord *rrsig; + int r; + + assert(key); + + /* Verifies all RRs from "a" that match the key "key", against DNSKEY RRs in "validated_dnskeys" */ + + if (!a || a->n_rrs <= 0) + return -ENODATA; + + /* Iterate through each RRSIG RR. */ + DNS_ANSWER_FOREACH(rrsig, a) { + DnsResourceRecord *dnskey; + + r = dnssec_key_match_rrsig(key, rrsig); + if (r < 0) + return r; + if (r == 0) + continue; + + found_rrsig = true; + + DNS_ANSWER_FOREACH(dnskey, validated_dnskeys) { + + r = dnssec_rrsig_match_dnskey(rrsig, dnskey); + if (r < 0) + return r; + if (r == 0) + continue; + + found_dnskey = true; + + /* Take the time here, if it isn't set yet, so + * that we do all validations with the same + * time. */ + if (realtime == USEC_INFINITY) + realtime = now(CLOCK_REALTIME); + + /* Yay, we found a matching RRSIG with a matching + * DNSKEY, awesome. Now let's verify all entries of + * the RRSet against the RRSIG and DNSKEY + * combination. */ + + r = dnssec_verify_rrset(a, key, rrsig, dnskey, realtime); + if (r < 0 && r != EOPNOTSUPP) + return r; + if (r == DNSSEC_VERIFIED) + return DNSSEC_VERIFIED; + + /* If the signature is invalid, or done using + an unsupported algorithm, let's try another + key and/or signature. After all they + key_tags and stuff are not unique, and + might be shared by multiple keys. */ + } + } + + if (found_dnskey) + return DNSSEC_INVALID; + + if (found_rrsig) + return DNSSEC_MISSING_KEY; + + return DNSSEC_NO_SIGNATURE; +} + +int dnssec_canonicalize(const char *n, char *buffer, size_t buffer_max) { + _cleanup_free_ char *s = NULL; + size_t c = 0; + int r; + + /* Converts the specified hostname into DNSSEC canonicalized + * form. */ + + if (buffer_max < 2) + return -ENOBUFS; + + for (;;) { + size_t i; + + r = dns_label_unescape(&n, buffer, buffer_max); + if (r < 0) + return r; + if (r == 0) + break; + if (r > 0) { + int k; + + /* DNSSEC validation is always done on the ASCII version of the label */ + k = dns_label_apply_idna(buffer, r, buffer, buffer_max); + if (k < 0) + return k; + if (k > 0) + r = k; + } + + if (buffer_max < (size_t) r + 2) + return -ENOBUFS; + + /* The DNSSEC canonical form is not clear on what to + * do with dots appearing in labels, the way DNS-SD + * does it. Refuse it for now. */ + + 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'; + } + + buffer[r] = '.'; + + buffer += r + 1; + c += r + 1; + + buffer_max -= r + 1; + } + + if (c <= 0) { + /* Not even a single label: this is the root domain name */ + + assert(buffer_max > 2); + buffer[0] = '.'; + buffer[1] = 0; + + return 1; + } + + return (int) c; +} + +int dnssec_verify_dnskey(DnsResourceRecord *dnskey, DnsResourceRecord *ds) { + gcry_md_hd_t md = NULL; + char owner_name[DNSSEC_CANONICAL_HOSTNAME_MAX]; + void *result; + int r; + + assert(dnskey); + assert(ds); + + /* Implements DNSKEY verification by a DS, according to RFC 4035, section 5.2 */ + + if (dnskey->key->type != DNS_TYPE_DNSKEY) + return -EINVAL; + if (ds->key->type != DNS_TYPE_DS) + return -EINVAL; + if ((dnskey->dnskey.flags & DNSKEY_FLAG_ZONE_KEY) == 0) + return -EKEYREJECTED; + if (dnskey->dnskey.protocol != 3) + return -EKEYREJECTED; + + if (!dnssec_algorithm_supported(dnskey->dnskey.algorithm)) + return -EOPNOTSUPP; + if (!dnssec_digest_supported(ds->ds.digest_type)) + return -EOPNOTSUPP; + + if (dnskey->dnskey.algorithm != ds->ds.algorithm) + return 0; + if (dnssec_keytag(dnskey) != ds->ds.key_tag) + return 0; + + switch (ds->ds.digest_type) { + + case DNSSEC_DIGEST_SHA1: + + if (ds->ds.digest_size != 20) + return 0; + + gcry_md_open(&md, GCRY_MD_SHA1, 0); + break; + + case DNSSEC_DIGEST_SHA256: + + if (ds->ds.digest_size != 32) + return 0; + + gcry_md_open(&md, GCRY_MD_SHA256, 0); + break; + + default: + assert_not_reached("Unknown digest"); + } + + if (!md) + return -EIO; + + r = dnssec_canonicalize(DNS_RESOURCE_KEY_NAME(dnskey->key), owner_name, sizeof(owner_name)); + if (r < 0) + goto finish; + + gcry_md_write(md, owner_name, r); + md_add_uint16(md, dnskey->dnskey.flags); + md_add_uint8(md, dnskey->dnskey.protocol); + md_add_uint8(md, dnskey->dnskey.algorithm); + gcry_md_write(md, dnskey->dnskey.key, dnskey->dnskey.key_size); + + result = gcry_md_read(md, 0); + if (!result) { + r = -EIO; + goto finish; + } + + r = memcmp(result, ds->ds.digest, ds->ds.digest_size) != 0; + +finish: + gcry_md_close(md); + return r; +} diff --git a/src/resolve/resolved-dns-dnssec.h b/src/resolve/resolved-dns-dnssec.h new file mode 100644 index 0000000000..8f812bc1fb --- /dev/null +++ b/src/resolve/resolved-dns-dnssec.h @@ -0,0 +1,49 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +#pragma once + +/*** + This file is part of systemd. + + Copyright 2015 Lennart Poettering + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see <http://www.gnu.org/licenses/>. +***/ + +#include "dns-domain.h" +#include "resolved-dns-answer.h" +#include "resolved-dns-rr.h" + +enum { + DNSSEC_VERIFIED, + DNSSEC_INVALID, + DNSSEC_NO_SIGNATURE, + DNSSEC_MISSING_KEY, + DNSSEC_SIGNATURE_EXPIRED, +}; + + +#define DNSSEC_CANONICAL_HOSTNAME_MAX (DNS_HOSTNAME_MAX + 2) + +int dnssec_rrsig_match_dnskey(DnsResourceRecord *rrsig, DnsResourceRecord *dnskey); +int dnssec_key_match_rrsig(DnsResourceKey *key, DnsResourceRecord *rrsig); + +int dnssec_verify_rrset(DnsAnswer *answer, DnsResourceKey *key, DnsResourceRecord *rrsig, DnsResourceRecord *dnskey, usec_t realtime); +int dnssec_verify_rrset_search(DnsAnswer *a, DnsResourceKey *key, DnsAnswer *validated_dnskeys, usec_t realtime); + +int dnssec_verify_dnskey(DnsResourceRecord *dnskey, DnsResourceRecord *ds); + +uint16_t dnssec_keytag(DnsResourceRecord *dnskey); + +int dnssec_canonicalize(const char *n, char *buffer, size_t buffer_max); diff --git a/src/resolve/resolved-dns-packet.c b/src/resolve/resolved-dns-packet.c index 75ca23fd08..2a010ef507 100644 --- a/src/resolve/resolved-dns-packet.c +++ b/src/resolve/resolved-dns-packet.c @@ -108,6 +108,8 @@ DnsPacket *dns_packet_ref(DnsPacket *p) { if (!p) return NULL; + assert(!p->on_stack); + assert(p->n_ref > 0); p->n_ref++; return p; @@ -126,7 +128,9 @@ static void dns_packet_free(DnsPacket *p) { hashmap_free(p->names); free(p->_data); - free(p); + + if (!p->on_stack) + free(p); } DnsPacket *dns_packet_unref(DnsPacket *p) { @@ -351,25 +355,10 @@ int dns_packet_append_uint32(DnsPacket *p, uint32_t v, size_t *start) { } int dns_packet_append_string(DnsPacket *p, const char *s, size_t *start) { - void *d; - size_t l; - int r; - assert(p); assert(s); - l = strlen(s); - if (l > 255) - return -E2BIG; - - r = dns_packet_extend(p, 1 + l, &d, start); - if (r < 0) - return r; - - ((uint8_t*) d)[0] = (uint8_t) l; - memcpy(((uint8_t*) d) + 1, s, l); - - return 0; + return dns_packet_append_raw_string(p, s, strlen(s), start); } int dns_packet_append_raw_string(DnsPacket *p, const void *s, size_t size, size_t *start) { @@ -395,7 +384,7 @@ int dns_packet_append_raw_string(DnsPacket *p, const void *s, size_t size, size_ } int dns_packet_append_label(DnsPacket *p, const char *d, size_t l, size_t *start) { - void *w; + uint8_t *w; int r; assert(p); @@ -404,12 +393,29 @@ int dns_packet_append_label(DnsPacket *p, const char *d, size_t l, size_t *start if (l > DNS_LABEL_MAX) return -E2BIG; - r = dns_packet_extend(p, 1 + l, &w, start); + r = dns_packet_extend(p, 1 + l, (void**) &w, start); if (r < 0) return r; - ((uint8_t*) w)[0] = (uint8_t) l; - memcpy(((uint8_t*) w) + 1, d, l); + *(w++) = (uint8_t) l; + + if (p->canonical_form) { + size_t i; + + /* Generate in canonical form, as defined by DNSSEC + * RFC 4034, Section 6.2, i.e. all lower-case. */ + + for (i = 0; i < l; i++) { + if (d[i] >= 'A' && d[i] <= 'Z') + w[i] = (uint8_t) (d[i] - 'A' + 'a'); + else + w[i] = (uint8_t) d[i]; + } + } else + /* Otherwise, just copy the string unaltered. This is + * essential for DNS-SD, where the casing of labels + * matters and needs to be retained. */ + memcpy(w, d, l); return 0; } @@ -662,8 +668,8 @@ fail: return r; } -int dns_packet_append_rr(DnsPacket *p, const DnsResourceRecord *rr, size_t *start) { - size_t saved_size, rdlength_offset, end, rdlength; +int dns_packet_append_rr(DnsPacket *p, const DnsResourceRecord *rr, size_t *start, size_t *rdata_start) { + size_t saved_size, rdlength_offset, end, rdlength, rds; int r; assert(p); @@ -684,6 +690,8 @@ int dns_packet_append_rr(DnsPacket *p, const DnsResourceRecord *rr, size_t *star if (r < 0) goto fail; + rds = p->size - saved_size; + switch (rr->unparseable ? _DNS_TYPE_INVALID : rr->key->type) { case DNS_TYPE_SRV: @@ -841,11 +849,11 @@ int dns_packet_append_rr(DnsPacket *p, const DnsResourceRecord *rr, size_t *star break; case DNS_TYPE_DNSKEY: - r = dns_packet_append_uint16(p, dnskey_to_flags(rr), NULL); + r = dns_packet_append_uint16(p, rr->dnskey.flags, NULL); if (r < 0) goto fail; - r = dns_packet_append_uint8(p, 3u, NULL); + r = dns_packet_append_uint8(p, rr->dnskey.protocol, NULL); if (r < 0) goto fail; @@ -962,6 +970,9 @@ int dns_packet_append_rr(DnsPacket *p, const DnsResourceRecord *rr, size_t *star if (start) *start = saved_size; + if (rdata_start) + *rdata_start = rds; + return 0; fail: @@ -1427,17 +1438,6 @@ static bool loc_size_ok(uint8_t size) { return m <= 9 && e <= 9 && (m > 0 || e == 0); } -static int dnskey_parse_flags(DnsResourceRecord *rr, uint16_t flags) { - assert(rr); - - if (flags & ~(DNSKEY_FLAG_SEP | DNSKEY_FLAG_ZONE_KEY)) - return -EBADMSG; - - rr->dnskey.zone_key_flag = flags & DNSKEY_FLAG_ZONE_KEY; - rr->dnskey.sep_flag = flags & DNSKEY_FLAG_SEP; - return 0; -} - int dns_packet_read_rr(DnsPacket *p, DnsResourceRecord **ret, size_t *start) { _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; @@ -1706,28 +1706,15 @@ int dns_packet_read_rr(DnsPacket *p, DnsResourceRecord **ret, size_t *start) { break; - case DNS_TYPE_DNSKEY: { - uint16_t flags; - uint8_t proto; - - r = dns_packet_read_uint16(p, &flags, NULL); - if (r < 0) - goto fail; - - r = dnskey_parse_flags(rr, flags); + case DNS_TYPE_DNSKEY: + r = dns_packet_read_uint16(p, &rr->dnskey.flags, NULL); if (r < 0) goto fail; - r = dns_packet_read_uint8(p, &proto, NULL); + r = dns_packet_read_uint8(p, &rr->dnskey.protocol, NULL); if (r < 0) goto fail; - /* protocol is required to be always 3 */ - if (proto != 3) { - r = -EBADMSG; - goto fail; - } - r = dns_packet_read_uint8(p, &rr->dnskey.algorithm, NULL); if (r < 0) goto fail; @@ -1744,7 +1731,6 @@ int dns_packet_read_rr(DnsPacket *p, DnsResourceRecord **ret, size_t *start) { } break; - } case DNS_TYPE_RRSIG: r = dns_packet_read_uint16(p, &rr->rrsig.type_covered, NULL); @@ -1976,17 +1962,3 @@ static const char* const dns_protocol_table[_DNS_PROTOCOL_MAX] = { [DNS_PROTOCOL_LLMNR] = "llmnr", }; DEFINE_STRING_TABLE_LOOKUP(dns_protocol, DnsProtocol); - -static const char* const dnssec_algorithm_table[_DNSSEC_ALGORITHM_MAX_DEFINED] = { - [DNSSEC_ALGORITHM_RSAMD5] = "RSAMD5", - [DNSSEC_ALGORITHM_DH] = "DH", - [DNSSEC_ALGORITHM_DSA] = "DSA", - [DNSSEC_ALGORITHM_ECC] = "ECC", - [DNSSEC_ALGORITHM_RSASHA1] = "RSASHA1", - [DNSSEC_ALGORITHM_DSA_NSEC3_SHA1] = "DSA-NSEC3-SHA1", - [DNSSEC_ALGORITHM_RSASHA1_NSEC3_SHA1] = "RSASHA1-NSEC3-SHA1", - [DNSSEC_ALGORITHM_INDIRECT] = "INDIRECT", - [DNSSEC_ALGORITHM_PRIVATEDNS] = "PRIVATEDNS", - [DNSSEC_ALGORITHM_PRIVATEOID] = "PRIVATEOID", -}; -DEFINE_STRING_TABLE_LOOKUP(dnssec_algorithm, int); diff --git a/src/resolve/resolved-dns-packet.h b/src/resolve/resolved-dns-packet.h index 25dfb2642f..a6b88e6c79 100644 --- a/src/resolve/resolved-dns-packet.h +++ b/src/resolve/resolved-dns-packet.h @@ -88,8 +88,10 @@ struct DnsPacket { uint16_t sender_port, destination_port; uint32_t ttl; - bool extracted; - bool refuse_compression; + bool on_stack:1; + bool extracted:1; + bool refuse_compression:1; + bool canonical_form:1; }; static inline uint8_t* DNS_PACKET_DATA(DnsPacket *p) { @@ -162,7 +164,7 @@ int dns_packet_append_raw_string(DnsPacket *p, const void *s, size_t size, size_ int dns_packet_append_label(DnsPacket *p, const char *s, size_t l, size_t *start); int dns_packet_append_name(DnsPacket *p, const char *name, bool allow_compression, size_t *start); int dns_packet_append_key(DnsPacket *p, const DnsResourceKey *key, size_t *start); -int dns_packet_append_rr(DnsPacket *p, const DnsResourceRecord *rr, size_t *start); +int dns_packet_append_rr(DnsPacket *p, const DnsResourceRecord *rr, size_t *start, size_t *rdata_start); int dns_packet_append_opt_rr(DnsPacket *p, uint16_t max_udp_size, bool edns0_do, size_t *start); void dns_packet_truncate(DnsPacket *p, size_t sz); @@ -223,32 +225,6 @@ DnsProtocol dns_protocol_from_string(const char *s) _pure_; #define LLMNR_MULTICAST_IPV4_ADDRESS ((struct in_addr) { .s_addr = htobe32(224U << 24 | 252U) }) #define LLMNR_MULTICAST_IPV6_ADDRESS ((struct in6_addr) { .s6_addr = { 0xFF, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03 } }) -#define DNSKEY_FLAG_ZONE_KEY (1u << 8) -#define DNSKEY_FLAG_SEP (1u << 0) - -static inline uint16_t dnskey_to_flags(const DnsResourceRecord *rr) { - return (rr->dnskey.zone_key_flag * DNSKEY_FLAG_ZONE_KEY | - rr->dnskey.sep_flag * DNSKEY_FLAG_SEP); -} - -/* http://tools.ietf.org/html/rfc4034#appendix-A.1 */ -enum { - DNSSEC_ALGORITHM_RSAMD5 = 1, - DNSSEC_ALGORITHM_DH, - DNSSEC_ALGORITHM_DSA, - DNSSEC_ALGORITHM_ECC, - DNSSEC_ALGORITHM_RSASHA1, - DNSSEC_ALGORITHM_DSA_NSEC3_SHA1, - DNSSEC_ALGORITHM_RSASHA1_NSEC3_SHA1, - DNSSEC_ALGORITHM_INDIRECT = 252, - DNSSEC_ALGORITHM_PRIVATEDNS, - DNSSEC_ALGORITHM_PRIVATEOID, - _DNSSEC_ALGORITHM_MAX_DEFINED -}; - -const char* dnssec_algorithm_to_string(int i) _const_; -int dnssec_algorithm_from_string(const char *s) _pure_; - static inline uint64_t SD_RESOLVED_FLAGS_MAKE(DnsProtocol protocol, int family) { /* Converts a protocol + family into a flags field as used in queries */ diff --git a/src/resolve/resolved-dns-question.h b/src/resolve/resolved-dns-question.h index e77116c03a..5ffb63e250 100644 --- a/src/resolve/resolved-dns-question.h +++ b/src/resolve/resolved-dns-question.h @@ -23,9 +23,10 @@ typedef struct DnsQuestion DnsQuestion; +#include "macro.h" #include "resolved-dns-rr.h" -/* A simple array of resources keys */ +/* A simple array of resource keys */ struct DnsQuestion { unsigned n_ref; @@ -55,10 +56,12 @@ const char *dns_question_first_name(DnsQuestion *q); DEFINE_TRIVIAL_CLEANUP_FUNC(DnsQuestion*, dns_question_unref); -#define DNS_QUESTION_FOREACH(key, q) \ - for (unsigned _i = ({ \ +#define _DNS_QUESTION_FOREACH(u, key, q) \ + for (unsigned UNIQ_T(i, u) = ({ \ (key) = ((q) && (q)->n_keys > 0) ? (q)->keys[0] : NULL; \ 0; \ }); \ - (q) && ((_i) < (q)->n_keys); \ - _i++, (key) = (_i < (q)->n_keys ? (q)->keys[_i] : NULL)) + (q) && (UNIQ_T(i, u) < (q)->n_keys); \ + UNIQ_T(i, u)++, (key) = (UNIQ_T(i, u) < (q)->n_keys ? (q)->keys[UNIQ_T(i, u)] : NULL)) + +#define DNS_QUESTION_FOREACH(key, q) _DNS_QUESTION_FOREACH(UNIQ, key, q) diff --git a/src/resolve/resolved-dns-rr.c b/src/resolve/resolved-dns-rr.c index 4a1abb0cdc..281e228b12 100644 --- a/src/resolve/resolved-dns-rr.c +++ b/src/resolve/resolved-dns-rr.c @@ -27,6 +27,7 @@ #include "hexdecoct.h" #include "resolved-dns-packet.h" #include "resolved-dns-rr.h" +#include "string-table.h" #include "string-util.h" #include "strv.h" @@ -416,6 +417,7 @@ DnsResourceRecord* dns_resource_record_unref(DnsResourceRecord *rr) { free(rr->generic.data); } + free(rr->wire_format); dns_resource_key_unref(rr->key); } @@ -576,8 +578,8 @@ int dns_resource_record_equal(const DnsResourceRecord *a, const DnsResourceRecor memcmp(a->sshfp.fingerprint, b->sshfp.fingerprint, a->sshfp.fingerprint_size) == 0; case DNS_TYPE_DNSKEY: - return a->dnskey.zone_key_flag == b->dnskey.zone_key_flag && - a->dnskey.sep_flag == b->dnskey.sep_flag && + return a->dnskey.flags == b->dnskey.flags && + a->dnskey.protocol == b->dnskey.protocol && a->dnskey.algorithm == b->dnskey.algorithm && a->dnskey.key_size == b->dnskey.key_size && memcmp(a->dnskey.key, b->dnskey.key, a->dnskey.key_size) == 0; @@ -883,9 +885,10 @@ int dns_resource_record_to_string(const DnsResourceRecord *rr, char **ret) { if (!t) return -ENOMEM; - r = asprintf(&s, "%s %u 3 %.*s%.*u %s", + r = asprintf(&s, "%s %u %u %.*s%.*u %s", k, - dnskey_to_flags(rr), + rr->dnskey.flags, + rr->dnskey.protocol, alg ? -1 : 0, alg, alg ? 0 : 1, alg ? 0u : (unsigned) rr->dnskey.algorithm, t); @@ -993,6 +996,51 @@ int dns_resource_record_to_string(const DnsResourceRecord *rr, char **ret) { return 0; } +int dns_resource_record_to_wire_format(DnsResourceRecord *rr, bool canonical) { + + DnsPacket packet = { + .n_ref = 1, + .protocol = DNS_PROTOCOL_DNS, + .on_stack = true, + .refuse_compression = true, + .canonical_form = canonical, + }; + + size_t start, rds; + int r; + + assert(rr); + + /* Generates the RR in wire-format, optionally in the + * canonical form as discussed in the DNSSEC RFC 4034, Section + * 6.2. We allocate a throw-away DnsPacket object on the stack + * here, because we need some book-keeping for memory + * management, and can reuse the DnsPacket serializer, that + * can generate the canonical form, too, but also knows label + * compression and suchlike. */ + + if (rr->wire_format && rr->wire_format_canonical == canonical) + return 0; + + r = dns_packet_append_rr(&packet, rr, &start, &rds); + if (r < 0) + return r; + + assert(start == 0); + assert(packet._data); + + free(rr->wire_format); + rr->wire_format = packet._data; + rr->wire_format_size = packet.size; + rr->wire_format_rdata_offset = rds; + rr->wire_format_canonical = canonical; + + packet._data = NULL; + dns_packet_unref(&packet); + + return 0; +} + const char *dns_class_to_string(uint16_t class) { switch (class) { @@ -1049,3 +1097,25 @@ bool dns_txt_item_equal(DnsTxtItem *a, DnsTxtItem *b) { return dns_txt_item_equal(a->items_next, b->items_next); } + +static const char* const dnssec_algorithm_table[_DNSSEC_ALGORITHM_MAX_DEFINED] = { + [DNSSEC_ALGORITHM_RSAMD5] = "RSAMD5", + [DNSSEC_ALGORITHM_DH] = "DH", + [DNSSEC_ALGORITHM_DSA] = "DSA", + [DNSSEC_ALGORITHM_ECC] = "ECC", + [DNSSEC_ALGORITHM_RSASHA1] = "RSASHA1", + [DNSSEC_ALGORITHM_DSA_NSEC3_SHA1] = "DSA-NSEC3-SHA1", + [DNSSEC_ALGORITHM_RSASHA1_NSEC3_SHA1] = "RSASHA1-NSEC3-SHA1", + [DNSSEC_ALGORITHM_RSASHA256] = "RSASHA256", + [DNSSEC_ALGORITHM_RSASHA512] = "RSASHA512", + [DNSSEC_ALGORITHM_INDIRECT] = "INDIRECT", + [DNSSEC_ALGORITHM_PRIVATEDNS] = "PRIVATEDNS", + [DNSSEC_ALGORITHM_PRIVATEOID] = "PRIVATEOID", +}; +DEFINE_STRING_TABLE_LOOKUP(dnssec_algorithm, int); + +static const char* const dnssec_digest_table[_DNSSEC_DIGEST_MAX_DEFINED] = { + [DNSSEC_DIGEST_SHA1] = "SHA1", + [DNSSEC_DIGEST_SHA256] = "SHA256", +}; +DEFINE_STRING_TABLE_LOOKUP(dnssec_digest, int); diff --git a/src/resolve/resolved-dns-rr.h b/src/resolve/resolved-dns-rr.h index a092961823..2a103aab8d 100644 --- a/src/resolve/resolved-dns-rr.h +++ b/src/resolve/resolved-dns-rr.h @@ -41,6 +41,37 @@ enum { _DNS_CLASS_INVALID = -1 }; +/* DNSKEY RR flags */ +#define DNSKEY_FLAG_ZONE_KEY (UINT16_C(1) << 8) +#define DNSKEY_FLAG_SEP (UINT16_C(1) << 0) + +/* DNSSEC algorithm identifiers, see + * http://tools.ietf.org/html/rfc4034#appendix-A.1 and + * https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml */ +enum { + DNSSEC_ALGORITHM_RSAMD5 = 1, + DNSSEC_ALGORITHM_DH, + DNSSEC_ALGORITHM_DSA, + DNSSEC_ALGORITHM_ECC, + DNSSEC_ALGORITHM_RSASHA1, + DNSSEC_ALGORITHM_DSA_NSEC3_SHA1, + DNSSEC_ALGORITHM_RSASHA1_NSEC3_SHA1, + DNSSEC_ALGORITHM_RSASHA256 = 8, /* RFC 5702 */ + DNSSEC_ALGORITHM_RSASHA512 = 10, /* RFC 5702 */ + DNSSEC_ALGORITHM_INDIRECT = 252, + DNSSEC_ALGORITHM_PRIVATEDNS, + DNSSEC_ALGORITHM_PRIVATEOID, + _DNSSEC_ALGORITHM_MAX_DEFINED +}; + +/* DNSSEC digest identifiers, see + * https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml */ +enum { + DNSSEC_DIGEST_SHA1 = 1, + DNSSEC_DIGEST_SHA256 = 2, + _DNSSEC_DIGEST_MAX_DEFINED +}; + struct DnsResourceKey { unsigned n_ref; uint16_t class, type; @@ -57,7 +88,11 @@ struct DnsResourceRecord { unsigned n_ref; DnsResourceKey *key; uint32_t ttl; - bool unparseable; + bool unparseable:1; + bool wire_format_canonical:1; + void *wire_format; + size_t wire_format_size; + size_t wire_format_rdata_offset; union { struct { void *data; @@ -135,8 +170,8 @@ struct DnsResourceRecord { /* http://tools.ietf.org/html/rfc4034#section-2.1 */ struct { - bool zone_key_flag:1; - bool sep_flag:1; + uint16_t flags; + uint8_t protocol; uint8_t algorithm; void* key; size_t key_size; @@ -209,6 +244,8 @@ int dns_resource_record_equal(const DnsResourceRecord *a, const DnsResourceRecor int dns_resource_record_to_string(const DnsResourceRecord *rr, char **ret); DEFINE_TRIVIAL_CLEANUP_FUNC(DnsResourceRecord*, dns_resource_record_unref); +int dns_resource_record_to_wire_format(DnsResourceRecord *rr, bool canonical); + DnsTxtItem *dns_txt_item_free_all(DnsTxtItem *i); bool dns_txt_item_equal(DnsTxtItem *a, DnsTxtItem *b); @@ -216,3 +253,9 @@ const char *dns_class_to_string(uint16_t type); int dns_class_from_string(const char *name, uint16_t *class); extern const struct hash_ops dns_resource_key_hash_ops; + +const char* dnssec_algorithm_to_string(int i) _const_; +int dnssec_algorithm_from_string(const char *s) _pure_; + +const char *dnssec_digest_to_string(int i) _const_; +int dnssec_digest_from_string(const char *s) _pure_; diff --git a/src/resolve/resolved-dns-scope.c b/src/resolve/resolved-dns-scope.c index 09e6872d26..846280e8b8 100644 --- a/src/resolve/resolved-dns-scope.c +++ b/src/resolve/resolved-dns-scope.c @@ -549,7 +549,7 @@ static int dns_scope_make_reply_packet( if (answer) { for (i = 0; i < answer->n_rrs; i++) { - r = dns_packet_append_rr(p, answer->items[i].rr, NULL); + r = dns_packet_append_rr(p, answer->items[i].rr, NULL, NULL); if (r < 0) return r; } @@ -559,7 +559,7 @@ static int dns_scope_make_reply_packet( if (soa) { for (i = 0; i < soa->n_rrs; i++) { - r = dns_packet_append_rr(p, soa->items[i].rr, NULL); + r = dns_packet_append_rr(p, soa->items[i].rr, NULL, NULL); if (r < 0) return r; } @@ -733,7 +733,7 @@ static int dns_scope_make_conflict_packet( if (r < 0) return r; - r = dns_packet_append_rr(p, rr, NULL); + r = dns_packet_append_rr(p, rr, NULL, NULL); if (r < 0) return r; diff --git a/src/resolve/test-dnssec.c b/src/resolve/test-dnssec.c new file mode 100644 index 0000000000..0b2ffeeddd --- /dev/null +++ b/src/resolve/test-dnssec.c @@ -0,0 +1,217 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2015 Lennart Poettering + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see <http://www.gnu.org/licenses/>. +***/ + +#include <arpa/inet.h> +#include <netinet/in.h> +#include <sys/socket.h> + +#include "alloc-util.h" +#include "resolved-dns-dnssec.h" +#include "resolved-dns-rr.h" +#include "string-util.h" + +static void test_dnssec_verify_rrset(void) { + + static const uint8_t signature_blob[] = { + 0x7f, 0x79, 0xdd, 0x5e, 0x89, 0x79, 0x18, 0xd0, 0x34, 0x86, 0x8c, 0x72, 0x77, 0x75, 0x48, 0x4d, + 0xc3, 0x7d, 0x38, 0x04, 0xab, 0xcd, 0x9e, 0x4c, 0x82, 0xb0, 0x92, 0xca, 0xe9, 0x66, 0xe9, 0x6e, + 0x47, 0xc7, 0x68, 0x8c, 0x94, 0xf6, 0x69, 0xcb, 0x75, 0x94, 0xe6, 0x30, 0xa6, 0xfb, 0x68, 0x64, + 0x96, 0x1a, 0x84, 0xe1, 0xdc, 0x16, 0x4c, 0x83, 0x6c, 0x44, 0xf2, 0x74, 0x4d, 0x74, 0x79, 0x8f, + 0xf3, 0xf4, 0x63, 0x0d, 0xef, 0x5a, 0xe7, 0xe2, 0xfd, 0xf2, 0x2b, 0x38, 0x7c, 0x28, 0x96, 0x9d, + 0xb6, 0xcd, 0x5c, 0x3b, 0x57, 0xe2, 0x24, 0x78, 0x65, 0xd0, 0x9e, 0x77, 0x83, 0x09, 0x6c, 0xff, + 0x3d, 0x52, 0x3f, 0x6e, 0xd1, 0xed, 0x2e, 0xf9, 0xee, 0x8e, 0xa6, 0xbe, 0x9a, 0xa8, 0x87, 0x76, + 0xd8, 0x77, 0xcc, 0x96, 0xa0, 0x98, 0xa1, 0xd1, 0x68, 0x09, 0x43, 0xcf, 0x56, 0xd9, 0xd1, 0x66, + }; + + static const uint8_t dnskey_blob[] = { + 0x03, 0x01, 0x00, 0x01, 0x9b, 0x49, 0x9b, 0xc1, 0xf9, 0x9a, 0xe0, 0x4e, 0xcf, 0xcb, 0x14, 0x45, + 0x2e, 0xc9, 0xf9, 0x74, 0xa7, 0x18, 0xb5, 0xf3, 0xde, 0x39, 0x49, 0xdf, 0x63, 0x33, 0x97, 0x52, + 0xe0, 0x8e, 0xac, 0x50, 0x30, 0x8e, 0x09, 0xd5, 0x24, 0x3d, 0x26, 0xa4, 0x49, 0x37, 0x2b, 0xb0, + 0x6b, 0x1b, 0xdf, 0xde, 0x85, 0x83, 0xcb, 0x22, 0x4e, 0x60, 0x0a, 0x91, 0x1a, 0x1f, 0xc5, 0x40, + 0xb1, 0xc3, 0x15, 0xc1, 0x54, 0x77, 0x86, 0x65, 0x53, 0xec, 0x10, 0x90, 0x0c, 0x91, 0x00, 0x5e, + 0x15, 0xdc, 0x08, 0x02, 0x4c, 0x8c, 0x0d, 0xc0, 0xac, 0x6e, 0xc4, 0x3e, 0x1b, 0x80, 0x19, 0xe4, + 0xf7, 0x5f, 0x77, 0x51, 0x06, 0x87, 0x61, 0xde, 0xa2, 0x18, 0x0f, 0x40, 0x8b, 0x79, 0x72, 0xfa, + 0x8d, 0x1a, 0x44, 0x47, 0x0d, 0x8e, 0x3a, 0x2d, 0xc7, 0x39, 0xbf, 0x56, 0x28, 0x97, 0xd9, 0x20, + 0x4f, 0x00, 0x51, 0x3b, + }; + + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *a = NULL, *rrsig = NULL, *dnskey = NULL; + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + _cleanup_free_ char *x = NULL, *y = NULL, *z = NULL; + + a = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_A, "nAsA.gov"); + assert_se(a); + + a->a.in_addr.s_addr = inet_addr("52.0.14.116"); + + assert_se(dns_resource_record_to_string(a, &x) >= 0); + log_info("A: %s", x); + + rrsig = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_RRSIG, "NaSa.GOV."); + assert_se(rrsig); + + rrsig->rrsig.type_covered = DNS_TYPE_A; + rrsig->rrsig.algorithm = DNSSEC_ALGORITHM_RSASHA256; + rrsig->rrsig.labels = 2; + rrsig->rrsig.original_ttl = 600; + rrsig->rrsig.expiration = 0x5683135c; + rrsig->rrsig.inception = 0x565b7da8; + rrsig->rrsig.key_tag = 63876; + rrsig->rrsig.signer = strdup("Nasa.Gov."); + assert_se(rrsig->rrsig.signer); + rrsig->rrsig.signature_size = sizeof(signature_blob); + rrsig->rrsig.signature = memdup(signature_blob, rrsig->rrsig.signature_size); + assert_se(rrsig->rrsig.signature); + + assert_se(dns_resource_record_to_string(rrsig, &y) >= 0); + log_info("RRSIG: %s", y); + + dnskey = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNSKEY, "nASA.gOV"); + assert_se(dnskey); + + dnskey->dnskey.flags = 256; + dnskey->dnskey.protocol = 3; + dnskey->dnskey.algorithm = DNSSEC_ALGORITHM_RSASHA256; + dnskey->dnskey.key_size = sizeof(dnskey_blob); + dnskey->dnskey.key = memdup(dnskey_blob, sizeof(dnskey_blob)); + assert_se(dnskey->dnskey.key); + + assert_se(dns_resource_record_to_string(dnskey, &z) >= 0); + log_info("DNSKEY: %s", z); + log_info("DNSKEY keytag: %u", dnssec_keytag(dnskey)); + + assert_se(dnssec_key_match_rrsig(a->key, rrsig) > 0); + assert_se(dnssec_rrsig_match_dnskey(rrsig, dnskey) > 0); + + answer = dns_answer_new(1); + assert_se(answer); + assert_se(dns_answer_add(answer, a, 0) >= 0); + + /* Validate the RR as it if was 2015-12-2 today */ + assert_se(dnssec_verify_rrset(answer, a->key, rrsig, dnskey, 1449092754*USEC_PER_SEC) == DNSSEC_VERIFIED); +} + +static void test_dnssec_verify_dns_key(void) { + + static const uint8_t ds1_fprint[] = { + 0x46, 0x8B, 0xC8, 0xDD, 0xC7, 0xE8, 0x27, 0x03, 0x40, 0xBB, 0x8A, 0x1F, 0x3B, 0x2E, 0x45, 0x9D, + 0x80, 0x67, 0x14, 0x01, + }; + static const uint8_t ds2_fprint[] = { + 0x8A, 0xEE, 0x80, 0x47, 0x05, 0x5F, 0x83, 0xD1, 0x48, 0xBA, 0x8F, 0xF6, 0xDD, 0xA7, 0x60, 0xCE, + 0x94, 0xF7, 0xC7, 0x5E, 0x52, 0x4C, 0xF2, 0xE9, 0x50, 0xB9, 0x2E, 0xCB, 0xEF, 0x96, 0xB9, 0x98, + }; + static const uint8_t dnskey_blob[] = { + 0x03, 0x01, 0x00, 0x01, 0xa8, 0x12, 0xda, 0x4f, 0xd2, 0x7d, 0x54, 0x14, 0x0e, 0xcc, 0x5b, 0x5e, + 0x45, 0x9c, 0x96, 0x98, 0xc0, 0xc0, 0x85, 0x81, 0xb1, 0x47, 0x8c, 0x7d, 0xe8, 0x39, 0x50, 0xcc, + 0xc5, 0xd0, 0xf2, 0x00, 0x81, 0x67, 0x79, 0xf6, 0xcc, 0x9d, 0xad, 0x6c, 0xbb, 0x7b, 0x6f, 0x48, + 0x97, 0x15, 0x1c, 0xfd, 0x0b, 0xfe, 0xd3, 0xd7, 0x7d, 0x9f, 0x81, 0x26, 0xd3, 0xc5, 0x65, 0x49, + 0xcf, 0x46, 0x62, 0xb0, 0x55, 0x6e, 0x47, 0xc7, 0x30, 0xef, 0x51, 0xfb, 0x3e, 0xc6, 0xef, 0xde, + 0x27, 0x3f, 0xfa, 0x57, 0x2d, 0xa7, 0x1d, 0x80, 0x46, 0x9a, 0x5f, 0x14, 0xb3, 0xb0, 0x2c, 0xbe, + 0x72, 0xca, 0xdf, 0xb2, 0xff, 0x36, 0x5b, 0x4f, 0xec, 0x58, 0x8e, 0x8d, 0x01, 0xe9, 0xa9, 0xdf, + 0xb5, 0x60, 0xad, 0x52, 0x4d, 0xfc, 0xa9, 0x3e, 0x8d, 0x35, 0x95, 0xb3, 0x4e, 0x0f, 0xca, 0x45, + 0x1b, 0xf7, 0xef, 0x3a, 0x88, 0x25, 0x08, 0xc7, 0x4e, 0x06, 0xc1, 0x62, 0x1a, 0xce, 0xd8, 0x77, + 0xbd, 0x02, 0x65, 0xf8, 0x49, 0xfb, 0xce, 0xf6, 0xa8, 0x09, 0xfc, 0xde, 0xb2, 0x09, 0x9d, 0x39, + 0xf8, 0x63, 0x9c, 0x32, 0x42, 0x7c, 0xa0, 0x30, 0x86, 0x72, 0x7a, 0x4a, 0xc6, 0xd4, 0xb3, 0x2d, + 0x24, 0xef, 0x96, 0x3f, 0xc2, 0xda, 0xd3, 0xf2, 0x15, 0x6f, 0xda, 0x65, 0x4b, 0x81, 0x28, 0x68, + 0xf4, 0xfe, 0x3e, 0x71, 0x4f, 0x50, 0x96, 0x72, 0x58, 0xa1, 0x89, 0xdd, 0x01, 0x61, 0x39, 0x39, + 0xc6, 0x76, 0xa4, 0xda, 0x02, 0x70, 0x3d, 0xc0, 0xdc, 0x8d, 0x70, 0x72, 0x04, 0x90, 0x79, 0xd4, + 0xec, 0x65, 0xcf, 0x49, 0x35, 0x25, 0x3a, 0x14, 0x1a, 0x45, 0x20, 0xeb, 0x31, 0xaf, 0x92, 0xba, + 0x20, 0xd3, 0xcd, 0xa7, 0x13, 0x44, 0xdc, 0xcf, 0xf0, 0x27, 0x34, 0xb9, 0xe7, 0x24, 0x6f, 0x73, + 0xe7, 0xea, 0x77, 0x03, + }; + + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *dnskey = NULL, *ds1 = NULL, *ds2 = NULL; + _cleanup_free_ char *a = NULL, *b = NULL, *c = NULL; + + /* The two DS RRs in effect for nasa.gov on 2015-12-01. */ + ds1 = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DS, "nasa.gov"); + assert_se(ds1); + + ds1->ds.key_tag = 47857; + ds1->ds.algorithm = DNSSEC_ALGORITHM_RSASHA256; + ds1->ds.digest_type = DNSSEC_DIGEST_SHA1; + ds1->ds.digest_size = sizeof(ds1_fprint); + ds1->ds.digest = memdup(ds1_fprint, ds1->ds.digest_size); + assert_se(ds1->ds.digest); + + assert_se(dns_resource_record_to_string(ds1, &a) >= 0); + log_info("DS1: %s", a); + + ds2 = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DS, "NASA.GOV"); + assert_se(ds2); + + ds2->ds.key_tag = 47857; + ds2->ds.algorithm = DNSSEC_ALGORITHM_RSASHA256; + ds2->ds.digest_type = DNSSEC_DIGEST_SHA256; + ds2->ds.digest_size = sizeof(ds2_fprint); + ds2->ds.digest = memdup(ds2_fprint, ds2->ds.digest_size); + assert_se(ds2->ds.digest); + + assert_se(dns_resource_record_to_string(ds2, &b) >= 0); + log_info("DS2: %s", b); + + dnskey = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNSKEY, "nasa.GOV"); + assert_se(dnskey); + + dnskey->dnskey.flags = 257; + dnskey->dnskey.protocol = 3; + dnskey->dnskey.algorithm = DNSSEC_ALGORITHM_RSASHA256; + dnskey->dnskey.key_size = sizeof(dnskey_blob); + dnskey->dnskey.key = memdup(dnskey_blob, sizeof(dnskey_blob)); + assert_se(dnskey->dnskey.key); + + assert_se(dns_resource_record_to_string(dnskey, &c) >= 0); + log_info("DNSKEY: %s", c); + log_info("DNSKEY keytag: %u", dnssec_keytag(dnskey)); + + assert_se(dnssec_verify_dnskey(dnskey, ds1) > 0); + assert_se(dnssec_verify_dnskey(dnskey, ds2) > 0); +} + +static void test_dnssec_canonicalize_one(const char *original, const char *canonical, int r) { + char canonicalized[DNSSEC_CANONICAL_HOSTNAME_MAX]; + + assert_se(dnssec_canonicalize(original, canonicalized, sizeof(canonicalized)) == r); + if (r < 0) + return; + + assert_se(streq(canonicalized, canonical)); +} + +static void test_dnssec_canonicalize(void) { + test_dnssec_canonicalize_one("", ".", 1); + test_dnssec_canonicalize_one(".", ".", 1); + test_dnssec_canonicalize_one("foo", "foo.", 4); + test_dnssec_canonicalize_one("foo.", "foo.", 4); + test_dnssec_canonicalize_one("FOO.", "foo.", 4); + test_dnssec_canonicalize_one("FOO.bar.", "foo.bar.", 8); + test_dnssec_canonicalize_one("FOO..bar.", NULL, -EINVAL); +} + +int main(int argc, char*argv[]) { + + test_dnssec_canonicalize(); + test_dnssec_verify_dns_key(); + test_dnssec_verify_rrset(); + + return 0; +} |