diff options
Diffstat (limited to 'src/resolve/resolved-dns-dnssec.c')
-rw-r--r-- | src/resolve/resolved-dns-dnssec.c | 548 |
1 files changed, 477 insertions, 71 deletions
diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c index 2d06775dca..814cb1c0f9 100644 --- a/src/resolve/resolved-dns-dnssec.c +++ b/src/resolve/resolved-dns-dnssec.c @@ -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" @@ -34,14 +35,13 @@ * * TODO: * - * - Iterative validation - * - NSEC proof of non-existance - * - NSEC3 proof of non-existance * - Make trust anchor store read additional DS+DNSKEY data from disk * - wildcard zones compatibility * - multi-label zone compatibility - * - DMSSEC cname/dname compatibility + * - cname/dname compatibility * - per-interface DNSSEC setting + * - fix TTL for cache entries to match RRSIG TTL + * - retry on failed validation? * - DSA support * - EC support? * @@ -64,6 +64,19 @@ * Normal RR → RRSIG/DNSKEY+ → DS → RRSIG/DNSKEY+ → DS → ... → DS → RRSIG/DNSKEY+ → DS */ +static void initialize_libgcrypt(void) { + const char *p; + + if (gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) + return; + + p = gcry_check_version("1.4.5"); + assert(p); + + gcry_control(GCRYCTL_DISABLE_SECMEM); + gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); +} + static bool dnssec_algorithm_supported(int algorithm) { return IN_SET(algorithm, DNSSEC_ALGORITHM_RSASHA1, @@ -72,12 +85,6 @@ static bool dnssec_algorithm_supported(int algorithm) { 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; @@ -193,11 +200,12 @@ static int dnssec_rsa_verify( } ge = gcry_pk_verify(signature_sexp, data_sexp, public_key_sexp); - if (ge == GPG_ERR_BAD_SIGNATURE) + if (gpg_err_code(ge) == GPG_ERR_BAD_SIGNATURE) r = 0; - else if (ge != 0) + else if (ge != 0) { + log_debug("RSA signature check failed: %s", gpg_strerror(ge)); r = -EIO; - else + } else r = 1; finish: @@ -272,7 +280,8 @@ int dnssec_verify_rrset( DnsResourceKey *key, DnsResourceRecord *rrsig, DnsResourceRecord *dnskey, - usec_t realtime) { + usec_t realtime, + DnssecResult *result) { uint8_t wire_format_name[DNS_WIRE_FOMAT_HOSTNAME_MAX]; size_t exponent_size, modulus_size, hash_size; @@ -285,6 +294,7 @@ int dnssec_verify_rrset( assert(key); assert(rrsig); assert(dnskey); + assert(result); assert(rrsig->key->type == DNS_TYPE_RRSIG); assert(dnskey->key->type == DNS_TYPE_DNSKEY); @@ -292,8 +302,10 @@ int dnssec_verify_rrset( * 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 (!dnssec_algorithm_supported(rrsig->rrsig.algorithm)) { + *result = DNSSEC_UNSUPPORTED_ALGORITHM; + return 0; + } if (a->n_rrs > VERIFY_RRS_MAX) return -E2BIG; @@ -301,8 +313,10 @@ int dnssec_verify_rrset( r = dnssec_rrsig_expired(rrsig, realtime); if (r < 0) return r; - if (r > 0) - return DNSSEC_SIGNATURE_EXPIRED; + if (r > 0) { + *result = DNSSEC_SIGNATURE_EXPIRED; + return 0; + } /* Collect all relevant RRs in a single array, so that we can look at the RRset */ list = newa(DnsResourceRecord *, a->n_rrs); @@ -326,7 +340,9 @@ int dnssec_verify_rrset( return -ENODATA; /* Bring the RRs into canonical order */ - qsort_safe(list, n, sizeof(DnsResourceRecord), rr_compare); + qsort_safe(list, n, sizeof(DnsResourceRecord*), rr_compare); + + initialize_libgcrypt(); /* OK, the RRs are now in canonical order. Let's calculate the digest */ switch (rrsig->rrsig.algorithm) { @@ -444,7 +460,8 @@ int dnssec_verify_rrset( if (r < 0) goto finish; - r = r ? DNSSEC_VERIFIED : DNSSEC_INVALID; + *result = r ? DNSSEC_VALIDATED : DNSSEC_INVALID; + r = 0; finish: gcry_md_close(md); @@ -476,10 +493,10 @@ int dnssec_rrsig_match_dnskey(DnsResourceRecord *rrsig, DnsResourceRecord *dnske 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)); + return dns_name_equal(DNS_RESOURCE_KEY_NAME(dnskey->key), rrsig->rrsig.signer); } -int dnssec_key_match_rrsig(DnsResourceKey *key, DnsResourceRecord *rrsig) { +int dnssec_key_match_rrsig(const DnsResourceKey *key, DnsResourceRecord *rrsig) { assert(key); assert(rrsig); @@ -499,15 +516,17 @@ int dnssec_verify_rrset_search( DnsAnswer *a, DnsResourceKey *key, DnsAnswer *validated_dnskeys, - usec_t realtime) { + usec_t realtime, + DnssecResult *result) { - bool found_rrsig = false, found_dnskey = false; + bool found_rrsig = false, found_invalid = false, found_expired_rrsig = false, found_unsupported_algorithm = false; DnsResourceRecord *rrsig; int r; assert(key); + assert(result); - /* Verifies all RRs from "a" that match the key "key", against DNSKEY RRs in "validated_dnskeys" */ + /* Verifies all RRs from "a" that match the key "key" against DNSKEYs in "validated_dnskeys" */ if (!a || a->n_rrs <= 0) return -ENODATA; @@ -515,7 +534,9 @@ int dnssec_verify_rrset_search( /* Iterate through each RRSIG RR. */ DNS_ANSWER_FOREACH(rrsig, a) { DnsResourceRecord *dnskey; + DnsAnswerFlags flags; + /* Is this an RRSIG RR that applies to RRs matching our key? */ r = dnssec_key_match_rrsig(key, rrsig); if (r < 0) return r; @@ -524,16 +545,20 @@ int dnssec_verify_rrset_search( found_rrsig = true; - DNS_ANSWER_FOREACH(dnskey, validated_dnskeys) { + /* Look for a matching key */ + DNS_ANSWER_FOREACH_FLAGS(dnskey, flags, validated_dnskeys) { + DnssecResult one_result; + if ((flags & DNS_ANSWER_AUTHENTICATED) == 0) + continue; + + /* Is this a DNSKEY RR that matches they key of our RRSIG? */ 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. */ @@ -545,27 +570,78 @@ int dnssec_verify_rrset_search( * the RRSet against the RRSIG and DNSKEY * combination. */ - r = dnssec_verify_rrset(a, key, rrsig, dnskey, realtime); - if (r < 0 && r != EOPNOTSUPP) + r = dnssec_verify_rrset(a, key, rrsig, dnskey, realtime, &one_result); + if (r < 0) 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. */ + + switch (one_result) { + + case DNSSEC_VALIDATED: + /* Yay, the RR has been validated, + * return immediately. */ + *result = DNSSEC_VALIDATED; + return 0; + + case DNSSEC_INVALID: + /* If the signature is invalid, 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. */ + found_invalid = true; + continue; + + case DNSSEC_UNSUPPORTED_ALGORITHM: + /* If the key algorithm is + unsupported, try another + RRSIG/DNSKEY pair, but remember we + encountered this, so that we can + return a proper error when we + encounter nothing better. */ + found_unsupported_algorithm = true; + continue; + + case DNSSEC_SIGNATURE_EXPIRED: + /* If the signature is expired, try + another one, but remember it, so + that we can return this */ + found_expired_rrsig = true; + continue; + + default: + assert_not_reached("Unexpected DNSSEC validation result"); + } } } - if (found_dnskey) - return DNSSEC_INVALID; + if (found_expired_rrsig) + *result = DNSSEC_SIGNATURE_EXPIRED; + else if (found_unsupported_algorithm) + *result = DNSSEC_UNSUPPORTED_ALGORITHM; + else if (found_invalid) + *result = DNSSEC_INVALID; + else if (found_rrsig) + *result = DNSSEC_MISSING_KEY; + else + *result = DNSSEC_NO_SIGNATURE; + + return 0; +} + +int dnssec_has_rrsig(DnsAnswer *a, const DnsResourceKey *key) { + DnsResourceRecord *rr; + int r; - if (found_rrsig) - return DNSSEC_MISSING_KEY; + /* Checks whether there's at least one RRSIG in 'a' that proctects RRs of the specified key */ - return DNSSEC_NO_SIGNATURE; + DNS_ANSWER_FOREACH(rr, a) { + r = dnssec_key_match_rrsig(key, rr); + if (r < 0) + return r; + if (r > 0) + return 1; + } + + return 0; } int dnssec_canonicalize(const char *n, char *buffer, size_t buffer_max) { @@ -633,9 +709,28 @@ int dnssec_canonicalize(const char *n, char *buffer, size_t buffer_max) { return (int) c; } +static int digest_to_gcrypt(uint8_t algorithm) { + + /* Translates a DNSSEC digest algorithm into a gcrypt digest iedntifier */ + + switch (algorithm) { + + case DNSSEC_DIGEST_SHA1: + return GCRY_MD_SHA1; + + case DNSSEC_DIGEST_SHA256: + return GCRY_MD_SHA256; + + default: + return -EOPNOTSUPP; + } +} + int dnssec_verify_dnskey(DnsResourceRecord *dnskey, DnsResourceRecord *ds) { - gcry_md_hd_t md = NULL; char owner_name[DNSSEC_CANONICAL_HOSTNAME_MAX]; + gcry_md_hd_t md = NULL; + size_t hash_size; + int algorithm; void *result; int r; @@ -653,45 +748,31 @@ int dnssec_verify_dnskey(DnsResourceRecord *dnskey, DnsResourceRecord *ds) { 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; + initialize_libgcrypt(); - case DNSSEC_DIGEST_SHA256: + algorithm = digest_to_gcrypt(ds->ds.digest_type); + if (algorithm < 0) + return algorithm; - if (ds->ds.digest_size != 32) - return 0; + hash_size = gcry_md_get_algo_dlen(algorithm); + assert(hash_size > 0); - gcry_md_open(&md, GCRY_MD_SHA256, 0); - break; + if (ds->ds.digest_size != hash_size) + return 0; - default: - assert_not_reached("Unknown digest"); - } + r = dnssec_canonicalize(DNS_RESOURCE_KEY_NAME(dnskey->key), owner_name, sizeof(owner_name)); + if (r < 0) + return r; + gcry_md_open(&md, algorithm, 0); 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); @@ -711,9 +792,334 @@ finish: return r; } +int dnssec_verify_dnskey_search(DnsResourceRecord *dnskey, DnsAnswer *validated_ds) { + DnsResourceRecord *ds; + DnsAnswerFlags flags; + int r; + + assert(dnskey); + + if (dnskey->key->type != DNS_TYPE_DNSKEY) + return 0; + + DNS_ANSWER_FOREACH_FLAGS(ds, flags, validated_ds) { + + if ((flags & DNS_ANSWER_AUTHENTICATED) == 0) + continue; + + if (ds->key->type != DNS_TYPE_DS) + continue; + + r = dnssec_verify_dnskey(dnskey, ds); + if (r < 0) + return r; + if (r > 0) + return 1; + } + + 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; +} + +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; + DnsResourceRecord *rr; + DnsAnswerFlags flags; + int hashed_size, r; + + assert(key); + assert(result); + + /* First step, look for 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; + + 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), p); + 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, ".", p, NULL); + if (!hashed_domain) + return -ENOMEM; + + r = dns_name_equal(DNS_RESOURCE_KEY_NAME(rr->key), hashed_domain); + if (r < 0) + return r; + if (r > 0) + goto found; + } + + /* We didn't find the closest encloser with this name, + * but let's remember this domain name, it might be + * the next closer name */ + + pp = p; + + /* Strip one label from the front */ + r = dns_name_parent(&p); + if (r < 0) + return r; + if (r == 0) + break; + } + + *result = DNSSEC_NSEC_NO_RR; + return 0; + +found: + /* We found a closest encloser in 'p'; next closer is 'pp' */ + + /* Ensure this is not a DNAME domain, see RFC5155, section 8.3. */ + if (bitmap_isset(rr->nsec3.types, DNS_TYPE_DNAME)) + return -EBADMSG; + + /* Ensure that this data is from the delegated domain + * (i.e. originates from the "lower" DNS server), and isn't + * just glue records (i.e. doesn't originate from the "upper" + * DNS server). */ + if (bitmap_isset(rr->nsec3.types, DNS_TYPE_NS) && + !bitmap_isset(rr->nsec3.types, DNS_TYPE_SOA)) + return -EBADMSG; + + 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; + return 0; + } + + r = dnssec_nsec3_hash(rr, pp, hashed); + if (r < 0) + return r; + if (r != hashed_size) + return -EBADMSG; + + l = base32hexmem(hashed, hashed_size, false); + if (!l) + return -ENOMEM; + + next_closer_domain = strjoin(l, ".", p, NULL); + if (!next_closer_domain) + return -ENOMEM; + + 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); + 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, ".", p, NULL); + if (!next_hashed_domain) + return -ENOMEM; + + r = dns_name_between(DNS_RESOURCE_KEY_NAME(rr->key), next_closer_domain, next_hashed_domain); + if (r < 0) + return r; + if (r > 0) { + if (rr->nsec3.flags & 1) + *result = DNSSEC_NSEC_OPTOUT; + else + *result = DNSSEC_NSEC_NXDOMAIN; + + return 1; + } + } + + *result = DNSSEC_NSEC_NO_RR; + return 0; +} + +int dnssec_test_nsec(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result) { + DnsResourceRecord *rr; + bool have_nsec3 = false; + DnsAnswerFlags flags; + int r; + + assert(key); + assert(result); + + /* Look for any NSEC/NSEC3 RRs that say something about the specified key. */ + + DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) { + + if (rr->key->class != key->class) + continue; + + if ((flags & DNS_ANSWER_AUTHENTICATED) == 0) + 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: + have_nsec3 = true; + break; + } + } + + /* OK, this was not sufficient. Let's see if NSEC3 can help. */ + if (have_nsec3) + return dnssec_test_nsec3(answer, key, result); + + /* 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", [DNSSEC_YES] = "yes", }; DEFINE_STRING_TABLE_LOOKUP(dnssec_mode, DnssecMode); + +static const char* const dnssec_result_table[_DNSSEC_RESULT_MAX] = { + [DNSSEC_VALIDATED] = "validated", + [DNSSEC_INVALID] = "invalid", + [DNSSEC_SIGNATURE_EXPIRED] = "signature-expired", + [DNSSEC_UNSUPPORTED_ALGORITHM] = "unsupported-algorithm", + [DNSSEC_NO_SIGNATURE] = "no-signature", + [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); |