Systemd/src/resolve/resolved-dns-dnssec.c

2264 lines
77 KiB
C
Raw Normal View History

/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "alloc-util.h"
#include "dns-domain.h"
#include "fd-util.h"
#include "fileio.h"
#include "gcrypt-util.h"
#include "hexdecoct.h"
#include "memory-util.h"
#include "resolved-dns-dnssec.h"
#include "resolved-dns-packet.h"
#include "sort-util.h"
#include "string-table.h"
#define VERIFY_RRS_MAX 256
#define MAX_KEY_SIZE (32*1024)
/* 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. RFC5155 says 2500 shall be the maximum useful value */
#define NSEC3_ITERATIONS_MAX 2500
/*
* 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
*/
uint16_t dnssec_keytag(DnsResourceRecord *dnskey, bool mask_revoke) {
const uint8_t *p;
uint32_t sum, f;
size_t i;
/* The algorithm from RFC 4034, Appendix B. */
assert(dnskey);
assert(dnskey->key->type == DNS_TYPE_DNSKEY);
f = (uint32_t) dnskey->dnskey.flags;
if (mask_revoke)
f &= ~DNSKEY_FLAG_REVOKE;
sum = f + ((((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);
}
#if HAVE_GCRYPT
2018-09-18 01:39:24 +02:00
static int rr_compare(DnsResourceRecord * const *a, DnsResourceRecord * const *b) {
const DnsResourceRecord *x = *a, *y = *b;
size_t m;
int r;
/* Let's order the RRs according to RFC 4034, Section 6.3 */
assert(x);
2018-09-18 01:39:24 +02:00
assert(x->wire_format);
assert(y);
2018-09-18 01:39:24 +02:00
assert(y->wire_format);
2018-09-18 01:39:24 +02:00
m = MIN(DNS_RESOURCE_RECORD_RDATA_SIZE(x), DNS_RESOURCE_RECORD_RDATA_SIZE(y));
2018-09-18 01:39:24 +02:00
r = memcmp(DNS_RESOURCE_RECORD_RDATA(x), DNS_RESOURCE_RECORD_RDATA(y), m);
if (r != 0)
return r;
2018-09-18 01:39:24 +02:00
return CMP(DNS_RESOURCE_RECORD_RDATA_SIZE(x), DNS_RESOURCE_RECORD_RDATA_SIZE(y));
}
static int dnssec_rsa_verify_raw(
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 (gpg_err_code(ge) == GPG_ERR_BAD_SIGNATURE)
r = 0;
else if (ge != 0) {
log_debug("RSA signature check failed: %s", gpg_strerror(ge));
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 int dnssec_rsa_verify(
const char *hash_algorithm,
const void *hash, size_t hash_size,
DnsResourceRecord *rrsig,
DnsResourceRecord *dnskey) {
size_t exponent_size, modulus_size;
void *exponent, *modulus;
assert(hash_algorithm);
assert(hash);
assert(hash_size > 0);
assert(rrsig);
assert(dnskey);
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)[1]) << 8) |
((size_t) ((uint8_t*) dnskey->dnskey.key)[2]);
if (exponent_size < 256)
return -EINVAL;
if (3 + exponent_size >= dnskey->dnskey.key_size)
return -EINVAL;
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)
return -EINVAL;
if (1 + exponent_size >= dnskey->dnskey.key_size)
return -EINVAL;
modulus = (uint8_t*) dnskey->dnskey.key + 1 + exponent_size;
modulus_size = dnskey->dnskey.key_size - 1 - exponent_size;
}
return dnssec_rsa_verify_raw(
hash_algorithm,
rrsig->rrsig.signature, rrsig->rrsig.signature_size,
hash, hash_size,
exponent, exponent_size,
modulus, modulus_size);
}
2015-12-27 21:35:00 +01:00
static int dnssec_ecdsa_verify_raw(
const char *hash_algorithm,
const char *curve,
const void *signature_r, size_t signature_r_size,
const void *signature_s, size_t signature_s_size,
const void *data, size_t data_size,
const void *key, size_t key_size) {
gcry_sexp_t public_key_sexp = NULL, data_sexp = NULL, signature_sexp = NULL;
gcry_mpi_t q = NULL, r = NULL, s = NULL;
gcry_error_t ge;
int k;
assert(hash_algorithm);
ge = gcry_mpi_scan(&r, GCRYMPI_FMT_USG, signature_r, signature_r_size, NULL);
if (ge != 0) {
k = -EIO;
goto finish;
}
ge = gcry_mpi_scan(&s, GCRYMPI_FMT_USG, signature_s, signature_s_size, NULL);
if (ge != 0) {
k = -EIO;
goto finish;
}
ge = gcry_mpi_scan(&q, GCRYMPI_FMT_USG, key, key_size, NULL);
if (ge != 0) {
k = -EIO;
goto finish;
}
ge = gcry_sexp_build(&signature_sexp,
NULL,
"(sig-val (ecdsa (r %m) (s %m)))",
r,
s);
if (ge != 0) {
k = -EIO;
goto finish;
}
ge = gcry_sexp_build(&data_sexp,
NULL,
"(data (flags rfc6979) (hash %s %b))",
hash_algorithm,
(int) data_size,
data);
if (ge != 0) {
k = -EIO;
goto finish;
}
ge = gcry_sexp_build(&public_key_sexp,
NULL,
"(public-key (ecc (curve %s) (q %m)))",
curve,
q);
if (ge != 0) {
k = -EIO;
goto finish;
}
ge = gcry_pk_verify(signature_sexp, data_sexp, public_key_sexp);
if (gpg_err_code(ge) == GPG_ERR_BAD_SIGNATURE)
k = 0;
else if (ge != 0) {
log_debug("ECDSA signature check failed: %s", gpg_strerror(ge));
k = -EIO;
} else
k = 1;
finish:
if (r)
gcry_mpi_release(r);
if (s)
gcry_mpi_release(s);
if (q)
gcry_mpi_release(q);
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 k;
}
static int dnssec_ecdsa_verify(
const char *hash_algorithm,
int algorithm,
const void *hash, size_t hash_size,
DnsResourceRecord *rrsig,
DnsResourceRecord *dnskey) {
const char *curve;
size_t key_size;
uint8_t *q;
assert(hash);
assert(hash_size);
assert(rrsig);
assert(dnskey);
if (algorithm == DNSSEC_ALGORITHM_ECDSAP256SHA256) {
key_size = 32;
curve = "NIST P-256";
} else if (algorithm == DNSSEC_ALGORITHM_ECDSAP384SHA384) {
key_size = 48;
curve = "NIST P-384";
} else
return -EOPNOTSUPP;
if (dnskey->dnskey.key_size != key_size * 2)
return -EINVAL;
if (rrsig->rrsig.signature_size != key_size * 2)
return -EINVAL;
q = newa(uint8_t, key_size*2 + 1);
2015-12-27 21:35:00 +01:00
q[0] = 0x04; /* Prepend 0x04 to indicate an uncompressed key */
memcpy(q+1, dnskey->dnskey.key, key_size*2);
return dnssec_ecdsa_verify_raw(
hash_algorithm,
curve,
rrsig->rrsig.signature, key_size,
(uint8_t*) rrsig->rrsig.signature + key_size, key_size,
hash, hash_size,
q, key_size*2+1);
}
#if GCRYPT_VERSION_NUMBER >= 0x010600
static int dnssec_eddsa_verify_raw(
const char *curve,
const void *signature_r, size_t signature_r_size,
const void *signature_s, size_t signature_s_size,
const void *data, size_t data_size,
const void *key, size_t key_size) {
gcry_sexp_t public_key_sexp = NULL, data_sexp = NULL, signature_sexp = NULL;
gcry_error_t ge;
int k;
ge = gcry_sexp_build(&signature_sexp,
NULL,
"(sig-val (eddsa (r %b) (s %b)))",
(int) signature_r_size,
signature_r,
(int) signature_s_size,
signature_s);
if (ge != 0) {
k = -EIO;
goto finish;
}
ge = gcry_sexp_build(&data_sexp,
NULL,
"(data (flags eddsa) (hash-algo sha512) (value %b))",
(int) data_size,
data);
if (ge != 0) {
k = -EIO;
goto finish;
}
ge = gcry_sexp_build(&public_key_sexp,
NULL,
"(public-key (ecc (curve %s) (flags eddsa) (q %b)))",
curve,
(int) key_size,
key);
if (ge != 0) {
k = -EIO;
goto finish;
}
ge = gcry_pk_verify(signature_sexp, data_sexp, public_key_sexp);
if (gpg_err_code(ge) == GPG_ERR_BAD_SIGNATURE)
k = 0;
else if (ge != 0) {
log_debug("EdDSA signature check failed: %s", gpg_strerror(ge));
k = -EIO;
} else
k = 1;
finish:
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 k;
}
static int dnssec_eddsa_verify(
int algorithm,
const void *data, size_t data_size,
DnsResourceRecord *rrsig,
DnsResourceRecord *dnskey) {
const char *curve;
size_t key_size;
if (algorithm == DNSSEC_ALGORITHM_ED25519) {
curve = "Ed25519";
key_size = 32;
} else
return -EOPNOTSUPP;
if (dnskey->dnskey.key_size != key_size)
return -EINVAL;
if (rrsig->rrsig.signature_size != key_size * 2)
return -EINVAL;
return dnssec_eddsa_verify_raw(
curve,
rrsig->rrsig.signature, key_size,
(uint8_t*) rrsig->rrsig.signature + key_size, key_size,
data, data_size,
dnskey->dnskey.key, key_size);
}
#endif
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 fwrite_uint8(FILE *fp, uint8_t v) {
fwrite(&v, sizeof(v), 1, fp);
}
static void fwrite_uint16(FILE *fp, uint16_t v) {
v = htobe16(v);
fwrite(&v, sizeof(v), 1, fp);
}
static void fwrite_uint32(FILE *fp, uint32_t v) {
v = htobe32(v);
fwrite(&v, sizeof(v), 1, fp);
}
static int dnssec_rrsig_prepare(DnsResourceRecord *rrsig) {
int n_key_labels, n_signer_labels;
const char *name;
int r;
/* Checks whether the specified RRSIG RR is somewhat valid, and initializes the .n_skip_labels_source and
* .n_skip_labels_signer fields so that we can use them later on. */
assert(rrsig);
assert(rrsig->key->type == DNS_TYPE_RRSIG);
/* Check if this RRSIG RR is already prepared */
if (rrsig->n_skip_labels_source != (unsigned) -1)
return 0;
if (rrsig->rrsig.inception > rrsig->rrsig.expiration)
return -EINVAL;
name = dns_resource_key_name(rrsig->key);
n_key_labels = dns_name_count_labels(name);
if (n_key_labels < 0)
return n_key_labels;
if (rrsig->rrsig.labels > n_key_labels)
return -EINVAL;
n_signer_labels = dns_name_count_labels(rrsig->rrsig.signer);
if (n_signer_labels < 0)
return n_signer_labels;
if (n_signer_labels > rrsig->rrsig.labels)
return -EINVAL;
r = dns_name_skip(name, n_key_labels - n_signer_labels, &name);
if (r < 0)
return r;
if (r == 0)
return -EINVAL;
/* Check if the signer is really a suffix of us */
r = dns_name_equal(name, rrsig->rrsig.signer);
if (r < 0)
return r;
if (r == 0)
return -EINVAL;
rrsig->n_skip_labels_source = n_key_labels - rrsig->rrsig.labels;
rrsig->n_skip_labels_signer = n_key_labels - n_signer_labels;
return 0;
}
2015-12-02 22:47:28 +01:00
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;
/* Consider inverted validity intervals as expired */
2015-12-02 22:47:28 +01:00
if (inception > expiration)
return true;
2015-12-02 22:47:28 +01:00
/* Permit a certain amount of clock skew of 10% of the valid
* time range. This takes inspiration from unbound's
* resolver. */
2015-12-02 22:47:28 +01:00
skew = (expiration - inception) / 10;
if (skew > SKEW_MAX)
skew = SKEW_MAX;
2015-12-02 22:47:28 +01:00
if (inception < skew)
inception = 0;
else
inception -= skew;
if (expiration + skew < expiration)
expiration = USEC_INFINITY;
else
expiration += skew;
return realtime < inception || realtime > expiration;
}
static int algorithm_to_gcrypt_md(uint8_t algorithm) {
/* Translates a DNSSEC signature algorithm into a gcrypt
* digest identifier.
*
* Note that we implement all algorithms listed as "Must
* implement" and "Recommended to Implement" in RFC6944. We
* don't implement any algorithms that are listed as
* "Optional" or "Must Not Implement". Specifically, we do not
* implement RSAMD5, DSASHA1, DH, DSA-NSEC3-SHA1, and
* GOST-ECC. */
switch (algorithm) {
case DNSSEC_ALGORITHM_RSASHA1:
case DNSSEC_ALGORITHM_RSASHA1_NSEC3_SHA1:
return GCRY_MD_SHA1;
case DNSSEC_ALGORITHM_RSASHA256:
2015-12-27 21:35:00 +01:00
case DNSSEC_ALGORITHM_ECDSAP256SHA256:
return GCRY_MD_SHA256;
2015-12-27 21:35:00 +01:00
case DNSSEC_ALGORITHM_ECDSAP384SHA384:
return GCRY_MD_SHA384;
case DNSSEC_ALGORITHM_RSASHA512:
return GCRY_MD_SHA512;
default:
return -EOPNOTSUPP;
}
}
static void dnssec_fix_rrset_ttl(
DnsResourceRecord *list[],
unsigned n,
DnsResourceRecord *rrsig,
usec_t realtime) {
unsigned k;
assert(list);
assert(n > 0);
assert(rrsig);
for (k = 0; k < n; k++) {
DnsResourceRecord *rr = list[k];
/* Pick the TTL as the minimum of the RR's TTL, the
* RR's original TTL according to the RRSIG and the
* RRSIG's own TTL, see RFC 4035, Section 5.3.3 */
rr->ttl = MIN3(rr->ttl, rrsig->rrsig.original_ttl, rrsig->ttl);
rr->expiry = rrsig->rrsig.expiration * USEC_PER_SEC;
/* Copy over information about the signer and wildcard source of synthesis */
rr->n_skip_labels_source = rrsig->n_skip_labels_source;
rr->n_skip_labels_signer = rrsig->n_skip_labels_signer;
}
rrsig->expiry = rrsig->rrsig.expiration * USEC_PER_SEC;
}
2015-12-02 22:47:28 +01:00
int dnssec_verify_rrset(
DnsAnswer *a,
const DnsResourceKey *key,
2015-12-02 22:47:28 +01:00
DnsResourceRecord *rrsig,
DnsResourceRecord *dnskey,
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
usec_t realtime,
DnssecResult *result) {
2015-12-02 22:47:28 +01:00
2018-06-08 16:05:18 +02:00
uint8_t wire_format_name[DNS_WIRE_FORMAT_HOSTNAME_MAX];
DnsResourceRecord **list, *rr;
const char *source, *name;
_cleanup_(gcry_md_closep) gcry_md_hd_t md = NULL;
int r, md_algorithm;
size_t k, n = 0;
size_t sig_size = 0;
_cleanup_free_ char *sig_data = NULL;
_cleanup_fclose_ FILE *f = NULL;
size_t hash_size;
void *hash;
bool wildcard;
assert(key);
assert(rrsig);
assert(dnskey);
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
assert(result);
2015-12-02 22:47:28 +01:00
assert(rrsig->key->type == DNS_TYPE_RRSIG);
assert(dnskey->key->type == DNS_TYPE_DNSKEY);
2016-02-12 00:26:37 +01:00
/* Verifies that the RRSet matches the specified "key" in "a",
* using the signature "rrsig" and the key "dnskey". It's
2016-02-12 00:26:37 +01:00
* assumed that RRSIG and DNSKEY match. */
r = dnssec_rrsig_prepare(rrsig);
if (r == -EINVAL) {
*result = DNSSEC_INVALID;
return r;
}
if (r < 0)
return r;
2015-12-02 22:47:28 +01:00
r = dnssec_rrsig_expired(rrsig, realtime);
if (r < 0)
return r;
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
if (r > 0) {
*result = DNSSEC_SIGNATURE_EXPIRED;
return 0;
}
2015-12-02 22:47:28 +01:00
name = dns_resource_key_name(key);
/* Some keys may only appear signed in the zone apex, and are invalid anywhere else. (SOA, NS...) */
if (dns_type_apex_only(rrsig->rrsig.type_covered)) {
r = dns_name_equal(rrsig->rrsig.signer, name);
if (r < 0)
return r;
if (r == 0) {
*result = DNSSEC_INVALID;
return 0;
}
}
/* OTOH DS RRs may not appear in the zone apex, but are valid everywhere else. */
if (rrsig->rrsig.type_covered == DNS_TYPE_DS) {
r = dns_name_equal(rrsig->rrsig.signer, name);
if (r < 0)
return r;
if (r > 0) {
*result = DNSSEC_INVALID;
return 0;
}
}
/* Determine the "Source of Synthesis" and whether this is a wildcard RRSIG */
r = dns_name_suffix(name, 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(name, "*");
if (r < 0)
return r;
if (r > 0)
source = name;
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 *, dns_answer_size(a));
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 > VERIFY_RRS_MAX)
return -E2BIG;
}
if (n <= 0)
return -ENODATA;
/* Bring the RRs into canonical order */
2018-09-18 01:39:24 +02:00
typesafe_qsort(list, n, rr_compare);
2019-04-04 11:46:44 +02:00
f = open_memstream_unlocked(&sig_data, &sig_size);
if (!f)
return -ENOMEM;
fwrite_uint16(f, rrsig->rrsig.type_covered);
fwrite_uint8(f, rrsig->rrsig.algorithm);
fwrite_uint8(f, rrsig->rrsig.labels);
fwrite_uint32(f, rrsig->rrsig.original_ttl);
fwrite_uint32(f, rrsig->rrsig.expiration);
fwrite_uint32(f, rrsig->rrsig.inception);
fwrite_uint16(f, rrsig->rrsig.key_tag);
r = dns_name_to_wire_format(rrsig->rrsig.signer, wire_format_name, sizeof(wire_format_name), true);
if (r < 0)
return r;
fwrite(wire_format_name, 1, r, f);
/* 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)
return r;
for (k = 0; k < n; k++) {
size_t l;
rr = list[k];
/* Hash the source of synthesis. If this is a wildcard, then prefix it with the *. label */
if (wildcard)
fwrite((uint8_t[]) { 1, '*'}, sizeof(uint8_t), 2, f);
fwrite(wire_format_name, 1, r, f);
fwrite_uint16(f, rr->key->type);
fwrite_uint16(f, rr->key->class);
fwrite_uint32(f, rrsig->rrsig.original_ttl);
l = DNS_RESOURCE_RECORD_RDATA_SIZE(rr);
assert(l <= 0xFFFF);
fwrite_uint16(f, (uint16_t) l);
fwrite(DNS_RESOURCE_RECORD_RDATA(rr), 1, l, f);
}
r = fflush_and_check(f);
if (r < 0)
return r;
initialize_libgcrypt(false);
switch (rrsig->rrsig.algorithm) {
#if GCRYPT_VERSION_NUMBER >= 0x010600
case DNSSEC_ALGORITHM_ED25519:
break;
#else
case DNSSEC_ALGORITHM_ED25519:
#endif
case DNSSEC_ALGORITHM_ED448:
*result = DNSSEC_UNSUPPORTED_ALGORITHM;
return 0;
default:
/* OK, the RRs are now in canonical order. Let's calculate the digest */
md_algorithm = algorithm_to_gcrypt_md(rrsig->rrsig.algorithm);
if (md_algorithm == -EOPNOTSUPP) {
*result = DNSSEC_UNSUPPORTED_ALGORITHM;
return 0;
}
if (md_algorithm < 0)
return md_algorithm;
gcry_md_open(&md, md_algorithm, 0);
if (!md)
return -EIO;
hash_size = gcry_md_get_algo_dlen(md_algorithm);
assert(hash_size > 0);
gcry_md_write(md, sig_data, sig_size);
hash = gcry_md_read(md, 0);
if (!hash)
return -EIO;
}
2015-12-27 21:35:00 +01:00
switch (rrsig->rrsig.algorithm) {
case DNSSEC_ALGORITHM_RSASHA1:
case DNSSEC_ALGORITHM_RSASHA1_NSEC3_SHA1:
case DNSSEC_ALGORITHM_RSASHA256:
case DNSSEC_ALGORITHM_RSASHA512:
r = dnssec_rsa_verify(
gcry_md_algo_name(md_algorithm),
2015-12-27 21:35:00 +01:00
hash, hash_size,
rrsig,
dnskey);
break;
case DNSSEC_ALGORITHM_ECDSAP256SHA256:
case DNSSEC_ALGORITHM_ECDSAP384SHA384:
r = dnssec_ecdsa_verify(
gcry_md_algo_name(md_algorithm),
2015-12-27 21:35:00 +01:00
rrsig->rrsig.algorithm,
hash, hash_size,
rrsig,
dnskey);
break;
#if GCRYPT_VERSION_NUMBER >= 0x010600
case DNSSEC_ALGORITHM_ED25519:
r = dnssec_eddsa_verify(
rrsig->rrsig.algorithm,
sig_data, sig_size,
rrsig,
dnskey);
break;
#endif
2015-12-27 21:35:00 +01:00
}
if (r < 0)
return r;
/* Now, fix the ttl, expiry, and remember the synthesizing source and the signer */
if (r > 0)
dnssec_fix_rrset_ttl(list, n, rrsig, realtime);
if (r == 0)
*result = DNSSEC_INVALID;
else if (wildcard)
*result = DNSSEC_VALIDATED_WILDCARD;
else
*result = DNSSEC_VALIDATED;
return 0;
}
int dnssec_rrsig_match_dnskey(DnsResourceRecord *rrsig, DnsResourceRecord *dnskey, bool revoked_ok) {
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 (!revoked_ok && (dnskey->dnskey.flags & DNSKEY_FLAG_REVOKE))
return 0;
if (dnskey->dnskey.protocol != 3)
return 0;
if (dnskey->dnskey.algorithm != rrsig->rrsig.algorithm)
return 0;
if (dnssec_keytag(dnskey, false) != rrsig->rrsig.key_tag)
return 0;
return dns_name_equal(dns_resource_key_name(dnskey->key), rrsig->rrsig.signer);
}
int dnssec_key_match_rrsig(const 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));
}
2015-12-02 22:47:28 +01:00
int dnssec_verify_rrset_search(
DnsAnswer *a,
const DnsResourceKey *key,
2015-12-02 22:47:28 +01:00
DnsAnswer *validated_dnskeys,
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
usec_t realtime,
DnssecResult *result,
DnsResourceRecord **ret_rrsig) {
2015-12-02 22:47:28 +01:00
bool found_rrsig = false, found_invalid = false, found_expired_rrsig = false, found_unsupported_algorithm = false;
DnsResourceRecord *rrsig;
int r;
assert(key);
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
assert(result);
/* Verifies all RRs from "a" that match the key "key" against DNSKEYs in "validated_dnskeys" */
if (!a || a->n_rrs <= 0)
return -ENODATA;
/* 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;
if (r == 0)
continue;
found_rrsig = true;
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
/* Look for a matching key */
DNS_ANSWER_FOREACH_FLAGS(dnskey, flags, validated_dnskeys) {
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
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, false);
if (r < 0)
return r;
if (r == 0)
continue;
2015-12-02 22:47:28 +01:00
/* 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. */
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
r = dnssec_verify_rrset(a, key, rrsig, dnskey, realtime, &one_result);
if (r < 0)
return r;
switch (one_result) {
case DNSSEC_VALIDATED:
case DNSSEC_VALIDATED_WILDCARD:
/* Yay, the RR has been validated,
* return immediately, but fix up the expiry */
if (ret_rrsig)
*ret_rrsig = rrsig;
*result = one_result;
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
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_expired_rrsig)
*result = DNSSEC_SIGNATURE_EXPIRED;
else if (found_unsupported_algorithm)
*result = DNSSEC_UNSUPPORTED_ALGORITHM;
else if (found_invalid)
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
*result = DNSSEC_INVALID;
else if (found_rrsig)
*result = DNSSEC_MISSING_KEY;
else
*result = DNSSEC_NO_SIGNATURE;
if (ret_rrsig)
*ret_rrsig = NULL;
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
return 0;
}
int dnssec_has_rrsig(DnsAnswer *a, const DnsResourceKey *key) {
DnsResourceRecord *rr;
int r;
2019-04-27 02:22:40 +02:00
/* Checks whether there's at least one RRSIG in 'a' that protects RRs of the specified key */
DNS_ANSWER_FOREACH(rr, a) {
r = dnssec_key_match_rrsig(key, rr);
if (r < 0)
return r;
if (r > 0)
return 1;
}
return 0;
}
static int digest_to_gcrypt_md(uint8_t algorithm) {
/* Translates a DNSSEC digest algorithm into a gcrypt digest identifier */
switch (algorithm) {
case DNSSEC_DIGEST_SHA1:
return GCRY_MD_SHA1;
case DNSSEC_DIGEST_SHA256:
return GCRY_MD_SHA256;
2015-12-27 12:58:37 +01:00
case DNSSEC_DIGEST_SHA384:
return GCRY_MD_SHA384;
default:
return -EOPNOTSUPP;
}
}
int dnssec_verify_dnskey_by_ds(DnsResourceRecord *dnskey, DnsResourceRecord *ds, bool mask_revoke) {
2018-06-08 16:05:18 +02:00
uint8_t wire_format[DNS_WIRE_FORMAT_HOSTNAME_MAX];
_cleanup_(gcry_md_closep) gcry_md_hd_t md = NULL;
size_t hash_size;
int md_algorithm, r;
void *result;
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 (!mask_revoke && (dnskey->dnskey.flags & DNSKEY_FLAG_REVOKE))
return -EKEYREJECTED;
if (dnskey->dnskey.protocol != 3)
return -EKEYREJECTED;
if (dnskey->dnskey.algorithm != ds->ds.algorithm)
return 0;
if (dnssec_keytag(dnskey, mask_revoke) != ds->ds.key_tag)
return 0;
initialize_libgcrypt(false);
md_algorithm = digest_to_gcrypt_md(ds->ds.digest_type);
if (md_algorithm < 0)
return md_algorithm;
hash_size = gcry_md_get_algo_dlen(md_algorithm);
assert(hash_size > 0);
if (ds->ds.digest_size != hash_size)
return 0;
r = dns_name_to_wire_format(dns_resource_key_name(dnskey->key), wire_format, sizeof(wire_format), true);
if (r < 0)
return r;
gcry_md_open(&md, md_algorithm, 0);
if (!md)
return -EIO;
gcry_md_write(md, wire_format, r);
if (mask_revoke)
md_add_uint16(md, dnskey->dnskey.flags & ~DNSKEY_FLAG_REVOKE);
else
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)
return -EIO;
return memcmp(result, ds->ds.digest, ds->ds.digest_size) == 0;
}
int dnssec_verify_dnskey_by_ds_search(DnsResourceRecord *dnskey, DnsAnswer *validated_ds) {
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
DnsResourceRecord *ds;
DnsAnswerFlags flags;
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
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;
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
if (ds->key->type != DNS_TYPE_DS)
continue;
if (ds->key->class != dnskey->key->class)
continue;
r = dns_name_equal(dns_resource_key_name(dnskey->key), dns_resource_key_name(ds->key));
if (r < 0)
return r;
if (r == 0)
continue;
r = dnssec_verify_dnskey_by_ds(dnskey, ds, false);
if (IN_SET(r, -EKEYREJECTED, -EOPNOTSUPP))
return 0; /* The DNSKEY is revoked or otherwise invalid, or we don't support the digest algorithm */
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
if (r < 0)
return r;
if (r > 0)
return 1;
}
return 0;
}
static int nsec3_hash_to_gcrypt_md(uint8_t algorithm) {
/* Translates a DNSSEC NSEC3 hash algorithm into a gcrypt digest identifier */
switch (algorithm) {
case NSEC3_ALGORITHM_SHA1:
return GCRY_MD_SHA1;
default:
return -EOPNOTSUPP;
}
}
int dnssec_nsec3_hash(DnsResourceRecord *nsec3, const char *name, void *ret) {
2018-06-08 16:05:18 +02:00
uint8_t wire_format[DNS_WIRE_FORMAT_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;
if (nsec3->nsec3.iterations > NSEC3_ITERATIONS_MAX)
return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"Ignoring NSEC3 RR %s with excessive number of iterations.",
dns_resource_record_to_string(nsec3));
algorithm = nsec3_hash_to_gcrypt_md(nsec3->nsec3.algorithm);
if (algorithm < 0)
return algorithm;
initialize_libgcrypt(false);
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 nsec3_is_good(DnsResourceRecord *rr, DnsResourceRecord *nsec3) {
const char *a, *b;
int r;
assert(rr);
if (rr->key->type != DNS_TYPE_NSEC3)
return 0;
2016-01-13 14:52:51 +01:00
/* RFC 5155, Section 8.2 says we MUST ignore NSEC3 RRs with flags != 0 or 1 */
if (!IN_SET(rr->nsec3.flags, 0, 1))
return 0;
/* Ignore NSEC3 RRs whose algorithm we don't know */
if (nsec3_hash_to_gcrypt_md(rr->nsec3.algorithm) < 0)
return 0;
/* Ignore NSEC3 RRs with an excessive number of required iterations */
if (rr->nsec3.iterations > NSEC3_ITERATIONS_MAX)
return 0;
/* Ignore NSEC3 RRs generated from wildcards. If these NSEC3 RRs weren't correctly signed we can't make this
* check (since rr->n_skip_labels_source is -1), but that's OK, as we won't trust them anyway in that case. */
2017-10-04 16:01:32 +02:00
if (!IN_SET(rr->n_skip_labels_source, 0, (unsigned) -1))
return 0;
/* Ignore NSEC3 RRs that are located anywhere else than one label below the zone */
2017-10-04 16:01:32 +02:00
if (!IN_SET(rr->n_skip_labels_signer, 1, (unsigned) -1))
return 0;
if (!nsec3)
return 1;
/* If a second NSEC3 RR is specified, also check if they are from the same zone. */
if (nsec3 == rr) /* Shortcut */
return 1;
if (rr->key->class != nsec3->key->class)
return 0;
if (rr->nsec3.algorithm != nsec3->nsec3.algorithm)
return 0;
if (rr->nsec3.iterations != nsec3->nsec3.iterations)
return 0;
if (rr->nsec3.salt_size != nsec3->nsec3.salt_size)
return 0;
if (memcmp_safe(rr->nsec3.salt, nsec3->nsec3.salt, rr->nsec3.salt_size) != 0)
return 0;
a = dns_resource_key_name(rr->key);
r = dns_name_parent(&a); /* strip off hash */
2018-11-16 14:42:14 +01:00
if (r <= 0)
return r;
b = dns_resource_key_name(nsec3->key);
r = dns_name_parent(&b); /* strip off hash */
2018-11-16 14:42:14 +01:00
if (r <= 0)
return r;
/* Make sure both have the same parent */
return dns_name_equal(a, b);
}
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);
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;
assert(nsec3);
assert(domain);
assert(zone);
assert(ret);
hashed_size = dnssec_nsec3_hash(nsec3, domain, hashed);
if (hashed_size < 0)
return hashed_size;
return nsec3_hashed_domain_format(hashed, (size_t) hashed_size, zone, ret);
}
/* See RFC 5155, Section 8
* First try to find a NSEC3 record that matches our query precisely, if that fails, find the closest
* enclosure. Secondly, find a proof that there is no closer enclosure and either a proof that there
* is no wildcard domain as a direct descendant of the closest enclosure, or find an NSEC3 record that
* matches the wildcard domain.
*
* Based on this we can prove either the existence of the record in @key, or NXDOMAIN or NODATA, or
2020-11-13 08:19:49 +01:00
* that there is no proof either way. The latter is the case if a proof of non-existence of a given
* 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_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;
assert(key);
assert(result);
/* First step, find the zone name and the NSEC3 parameters of the zone.
* it is sufficient to look for the longest common suffix we find with
* any NSEC3 RR in the response. Any NSEC3 record will do as all NSEC3
* records from a given zone in a response must use the same
* parameters. */
zone = dns_resource_key_name(key);
for (;;) {
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(zone_rr->key), 1, zone);
if (r < 0)
return r;
if (r > 0)
goto found_zone;
}
/* Strip one label from the front */
r = dns_name_parent(&zone);
if (r < 0)
return r;
if (r == 0)
break;
}
*result = DNSSEC_NSEC_NO_RR;
return 0;
found_zone:
/* Second step, find the closest encloser NSEC3 RR in 'answer' that matches 'key' */
p = dns_resource_key_name(key);
for (;;) {
_cleanup_free_ char *hashed_domain = NULL;
hashed_size = nsec3_hashed_domain_make(zone_rr, p, zone, &hashed_domain);
if (hashed_size == -EOPNOTSUPP) {
*result = DNSSEC_NSEC_UNSUPPORTED_ALGORITHM;
return 0;
}
if (hashed_size < 0)
return hashed_size;
DNS_ANSWER_FOREACH_FLAGS(enclosure_rr, flags, answer) {
r = nsec3_is_good(enclosure_rr, zone_rr);
if (r < 0)
return r;
if (r == 0)
continue;
if (enclosure_rr->nsec3.next_hashed_name_size != (size_t) hashed_size)
continue;
r = dns_name_equal(dns_resource_key_name(enclosure_rr->key), hashed_domain);
if (r < 0)
return r;
if (r > 0) {
a = flags & DNS_ANSWER_AUTHENTICATED;
goto found_closest_encloser;
}
}
/* 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_closest_encloser:
/* We found a closest encloser in 'p'; next closer is 'pp' */
if (!pp) {
/* We have an exact match! If we area looking for a DS RR, then we must insist that we got the NSEC3 RR
* from the parent. Otherwise the one from the child. Do so, by checking whether SOA and NS are
* appropriately set. */
if (key->type == DNS_TYPE_DS) {
if (bitmap_isset(enclosure_rr->nsec3.types, DNS_TYPE_SOA))
return -EBADMSG;
} else {
if (bitmap_isset(enclosure_rr->nsec3.types, DNS_TYPE_NS) &&
!bitmap_isset(enclosure_rr->nsec3.types, DNS_TYPE_SOA))
return -EBADMSG;
}
/* No next closer NSEC3 RR. That means there's a direct NSEC3 RR for our key. */
if (bitmap_isset(enclosure_rr->nsec3.types, key->type))
*result = DNSSEC_NSEC_FOUND;
else if (bitmap_isset(enclosure_rr->nsec3.types, DNS_TYPE_CNAME))
*result = DNSSEC_NSEC_CNAME;
else
*result = DNSSEC_NSEC_NODATA;
if (authenticated)
*authenticated = a;
if (ttl)
*ttl = enclosure_rr->ttl;
return 0;
}
/* Ensure this is not a DNAME domain, see RFC5155, section 8.3. */
if (bitmap_isset(enclosure_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(enclosure_rr->nsec3.types, DNS_TYPE_NS) &&
!bitmap_isset(enclosure_rr->nsec3.types, DNS_TYPE_SOA))
return -EBADMSG;
/* Prove that there is no next closer and whether or not there is a 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_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 *next_hashed_domain = NULL;
r = nsec3_is_good(rr, zone_rr);
if (r < 0)
return r;
if (r == 0)
continue;
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)
return r;
if (r > 0) {
if (rr->nsec3.flags & 1)
optout = true;
a = a && (flags & DNS_ANSWER_AUTHENTICATED);
no_closer = true;
}
r = dns_name_equal(dns_resource_key_name(rr->key), wildcard_domain);
if (r < 0)
return r;
if (r > 0) {
a = a && (flags & DNS_ANSWER_AUTHENTICATED);
wildcard_rr = rr;
}
r = dns_name_between(dns_resource_key_name(rr->key), wildcard_domain, next_hashed_domain);
if (r < 0)
return r;
if (r > 0) {
if (rr->nsec3.flags & 1)
/* This only makes sense if we have a wildcard delegation, which is
* very unlikely, see RFC 4592, Section 4.2, but we cannot rely on
* this not happening, so hence cannot simply conclude NXDOMAIN as
* we would wish */
optout = true;
a = a && (flags & DNS_ANSWER_AUTHENTICATED);
no_wildcard = true;
}
}
if (wildcard_rr && no_wildcard)
return -EBADMSG;
if (!no_closer) {
*result = DNSSEC_NSEC_NO_RR;
return 0;
}
if (wildcard_rr) {
/* A wildcard exists that matches our query. */
if (optout)
/* This is not specified in any RFC to the best of my knowledge, but
* if the next closer enclosure is covered by an opt-out NSEC3 RR
* it means that we cannot prove that the source of synthesis is
* correct, as there may be a closer match. */
*result = DNSSEC_NSEC_OPTOUT;
else if (bitmap_isset(wildcard_rr->nsec3.types, key->type))
*result = DNSSEC_NSEC_FOUND;
else if (bitmap_isset(wildcard_rr->nsec3.types, DNS_TYPE_CNAME))
*result = DNSSEC_NSEC_CNAME;
else
*result = DNSSEC_NSEC_NODATA;
} else {
if (optout)
/* The RFC only specifies that we have to care for optout for NODATA for
* DS records. However, children of an insecure opt-out delegation should
* also be considered opt-out, rather than verified NXDOMAIN.
* Note that we do not require a proof of wildcard non-existence if the
* next closer domain is covered by an opt-out, as that would not provide
* any additional information. */
*result = DNSSEC_NSEC_OPTOUT;
else if (no_wildcard)
*result = DNSSEC_NSEC_NXDOMAIN;
else {
*result = DNSSEC_NSEC_NO_RR;
return 0;
}
}
if (authenticated)
*authenticated = a;
if (ttl)
*ttl = enclosure_rr->ttl;
return 0;
}
static int dnssec_nsec_wildcard_equal(DnsResourceRecord *rr, const char *name) {
char label[DNS_LABEL_MAX];
const char *n;
int r;
assert(rr);
assert(rr->key->type == DNS_TYPE_NSEC);
/* Checks whether the specified RR has a name beginning in "*.", and if the rest is a suffix of our name */
if (rr->n_skip_labels_source != 1)
return 0;
n = dns_resource_key_name(rr->key);
r = dns_label_unescape(&n, label, sizeof label, 0);
if (r <= 0)
return r;
if (r != 1 || label[0] != '*')
return 0;
return dns_name_endswith(name, n);
}
static int dnssec_nsec_in_path(DnsResourceRecord *rr, const char *name) {
const char *nn, *common_suffix;
int r;
assert(rr);
assert(rr->key->type == DNS_TYPE_NSEC);
/* Checks whether the specified nsec RR indicates that name is an empty non-terminal (ENT)
*
* A couple of examples:
*
* NSEC bar waldo.foo.bar: indicates that foo.bar exists and is an ENT
* NSEC waldo.foo.bar yyy.zzz.xoo.bar: indicates that xoo.bar and zzz.xoo.bar exist and are ENTs
* NSEC yyy.zzz.xoo.bar bar: indicates pretty much nothing about ENTs
*/
/* First, determine parent of next domain. */
nn = rr->nsec.next_domain_name;
r = dns_name_parent(&nn);
if (r <= 0)
return r;
/* If the name we just determined is not equal or child of the name we are interested in, then we can't say
* anything at all. */
r = dns_name_endswith(nn, name);
if (r <= 0)
return r;
/* If the name we are interested in is not a prefix of the common suffix of the NSEC RR's owner and next domain names, then we can't say anything either. */
r = dns_name_common_suffix(dns_resource_key_name(rr->key), rr->nsec.next_domain_name, &common_suffix);
if (r < 0)
return r;
return dns_name_endswith(name, common_suffix);
}
static int dnssec_nsec_from_parent_zone(DnsResourceRecord *rr, const char *name) {
int r;
assert(rr);
assert(rr->key->type == DNS_TYPE_NSEC);
/* Checks whether this NSEC originates to the parent zone or the child zone. */
r = dns_name_parent(&name);
if (r <= 0)
return r;
r = dns_name_equal(name, dns_resource_key_name(rr->key));
if (r <= 0)
return r;
/* DNAME, and NS without SOA is an indication for a delegation. */
if (bitmap_isset(rr->nsec.types, DNS_TYPE_DNAME))
return 1;
if (bitmap_isset(rr->nsec.types, DNS_TYPE_NS) && !bitmap_isset(rr->nsec.types, DNS_TYPE_SOA))
return 1;
return 0;
}
static int dnssec_nsec_covers(DnsResourceRecord *rr, const char *name) {
const char *signer;
int r;
assert(rr);
assert(rr->key->type == DNS_TYPE_NSEC);
/* Checks whether the name is covered by this NSEC RR. This means, that the name is somewhere below the NSEC's
* signer name, and between the NSEC's two names. */
r = dns_resource_record_signer(rr, &signer);
if (r < 0)
return r;
r = dns_name_endswith(name, signer); /* this NSEC isn't suitable the name is not in the signer's domain */
if (r <= 0)
return r;
return dns_name_between(dns_resource_key_name(rr->key), name, rr->nsec.next_domain_name);
}
static int dnssec_nsec_generate_wildcard(DnsResourceRecord *rr, const char *name, char **wc) {
const char *common_suffix1, *common_suffix2, *signer;
int r, labels1, labels2;
assert(rr);
assert(rr->key->type == DNS_TYPE_NSEC);
/* Generates "Wildcard at the Closest Encloser" for the given name and NSEC RR. */
r = dns_resource_record_signer(rr, &signer);
if (r < 0)
return r;
r = dns_name_endswith(name, signer); /* this NSEC isn't suitable the name is not in the signer's domain */
if (r <= 0)
return r;
r = dns_name_common_suffix(name, dns_resource_key_name(rr->key), &common_suffix1);
if (r < 0)
return r;
r = dns_name_common_suffix(name, rr->nsec.next_domain_name, &common_suffix2);
if (r < 0)
return r;
labels1 = dns_name_count_labels(common_suffix1);
if (labels1 < 0)
return labels1;
labels2 = dns_name_count_labels(common_suffix2);
if (labels2 < 0)
return labels2;
if (labels1 > labels2)
r = dns_name_concat("*", common_suffix1, 0, wc);
else
r = dns_name_concat("*", common_suffix2, 0, wc);
if (r < 0)
return r;
return 0;
}
int dnssec_nsec_test(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result, bool *authenticated, uint32_t *ttl) {
bool have_nsec3 = false, covering_rr_authenticated = false, wildcard_rr_authenticated = false;
DnsResourceRecord *rr, *covering_rr = NULL, *wildcard_rr = NULL;
DnsAnswerFlags flags;
const char *name;
int r;
assert(key);
assert(result);
/* Look for any NSEC/NSEC3 RRs that say something about the specified key. */
name = dns_resource_key_name(key);
DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) {
if (rr->key->class != key->class)
continue;
have_nsec3 = have_nsec3 || (rr->key->type == DNS_TYPE_NSEC3);
if (rr->key->type != DNS_TYPE_NSEC)
continue;
/* The following checks only make sense for NSEC RRs that are not expanded from a wildcard */
r = dns_resource_record_is_synthetic(rr);
if (r == -ENODATA) /* No signing RR known. */
continue;
if (r < 0)
return r;
if (r > 0)
continue;
/* Check if this is a direct match. If so, we have encountered a NODATA case */
r = dns_name_equal(dns_resource_key_name(rr->key), name);
if (r < 0)
return r;
if (r == 0) {
/* If it's not a direct match, maybe it's a wild card match? */
r = dnssec_nsec_wildcard_equal(rr, name);
if (r < 0)
return r;
}
if (r > 0) {
if (key->type == DNS_TYPE_DS) {
/* If we look for a DS RR and the server sent us the NSEC RR of the child zone
* we have a problem. For DS RRs we want the NSEC RR from the parent */
if (bitmap_isset(rr->nsec.types, DNS_TYPE_SOA))
continue;
} else {
/* For all RR types, ensure that if NS is set SOA is set too, so that we know
* we got the child's NSEC. */
if (bitmap_isset(rr->nsec.types, DNS_TYPE_NS) &&
!bitmap_isset(rr->nsec.types, DNS_TYPE_SOA))
continue;
}
if (bitmap_isset(rr->nsec.types, key->type))
*result = DNSSEC_NSEC_FOUND;
else if (bitmap_isset(rr->nsec.types, DNS_TYPE_CNAME))
*result = DNSSEC_NSEC_CNAME;
else
*result = DNSSEC_NSEC_NODATA;
if (authenticated)
*authenticated = flags & DNS_ANSWER_AUTHENTICATED;
if (ttl)
*ttl = rr->ttl;
return 0;
}
/* Check if the name we are looking for is an empty non-terminal within the owner or next name
* of the NSEC RR. */
r = dnssec_nsec_in_path(rr, name);
if (r < 0)
return r;
if (r > 0) {
*result = DNSSEC_NSEC_NODATA;
if (authenticated)
*authenticated = flags & DNS_ANSWER_AUTHENTICATED;
if (ttl)
*ttl = rr->ttl;
return 0;
}
/* The following two "covering" checks, are not useful if the NSEC is from the parent */
r = dnssec_nsec_from_parent_zone(rr, name);
if (r < 0)
return r;
if (r > 0)
continue;
/* Check if this NSEC RR proves the absence of an explicit RR under this name */
r = dnssec_nsec_covers(rr, name);
if (r < 0)
return r;
if (r > 0 && (!covering_rr || !covering_rr_authenticated)) {
covering_rr = rr;
covering_rr_authenticated = flags & DNS_ANSWER_AUTHENTICATED;
}
}
if (covering_rr) {
_cleanup_free_ char *wc = NULL;
r = dnssec_nsec_generate_wildcard(covering_rr, name, &wc);
if (r < 0)
return r;
DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) {
if (rr->key->class != key->class)
continue;
if (rr->key->type != DNS_TYPE_NSEC)
continue;
/* Check if this NSEC RR proves the nonexistence of the wildcard */
r = dnssec_nsec_covers(rr, wc);
if (r < 0)
return r;
if (r > 0 && (!wildcard_rr || !wildcard_rr_authenticated)) {
wildcard_rr = rr;
wildcard_rr_authenticated = flags & DNS_ANSWER_AUTHENTICATED;
}
}
}
if (covering_rr && wildcard_rr) {
/* If we could prove that neither the name itself, nor the wildcard at the closest encloser exists, we
* proved the NXDOMAIN case. */
*result = DNSSEC_NSEC_NXDOMAIN;
if (authenticated)
*authenticated = covering_rr_authenticated && wildcard_rr_authenticated;
if (ttl)
*ttl = MIN(covering_rr->ttl, wildcard_rr->ttl);
return 0;
}
/* OK, this was not sufficient. Let's see if NSEC3 can help. */
if (have_nsec3)
return dnssec_test_nsec3(answer, key, result, authenticated, ttl);
2019-04-27 02:22:40 +02:00
/* No appropriate NSEC RR found, report this. */
*result = DNSSEC_NSEC_NO_RR;
return 0;
}
static int dnssec_nsec_test_enclosed(DnsAnswer *answer, uint16_t type, 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;
if (rr->key->type != type && type != DNS_TYPE_ANY)
continue;
switch (rr->key->type) {
case DNS_TYPE_NSEC:
/* We only care for NSEC RRs from the indicated zone */
r = dns_resource_record_is_signer(rr, zone);
if (r < 0)
return r;
if (r == 0)
continue;
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;
/* We only care for NSEC3 RRs from the indicated zone */
r = dns_resource_record_is_signer(rr, zone);
if (r < 0)
return r;
if (r == 0)
continue;
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 int dnssec_test_positive_wildcard_nsec3(
DnsAnswer *answer,
const char *name,
const char *source,
const char *zone,
bool *authenticated) {
const char *next_closer = NULL;
int r;
/* Run a positive NSEC3 wildcard proof. Specifically:
*
2016-02-12 00:26:37 +01:00
* A proof that the "next closer" of the generating wildcard does not exist.
*
* Note a key difference between the NSEC3 and NSEC versions of the proof. NSEC RRs don't have to exist for
* empty non-transients. NSEC3 RRs however have to. This means it's sufficient to check if the next closer name
* exists for the NSEC3 RR and we are done.
*
* To prove that a.b.c.d.e.f is rightfully synthesized from a wildcard *.d.e.f all we have to check is that
* c.d.e.f does not exist. */
for (;;) {
next_closer = name;
r = dns_name_parent(&name);
2018-11-16 14:42:14 +01:00
if (r <= 0)
return r;
r = dns_name_equal(name, source);
if (r < 0)
return r;
if (r > 0)
break;
}
return dnssec_nsec_test_enclosed(answer, DNS_TYPE_NSEC3, next_closer, zone, authenticated);
}
static int dnssec_test_positive_wildcard_nsec(
DnsAnswer *answer,
const char *name,
const char *source,
const char *zone,
bool *_authenticated) {
bool authenticated = true;
int r;
/* Run a positive NSEC wildcard proof. Specifically:
*
* A proof that there's neither a wildcard name nor a non-wildcard name that is a suffix of the name "name" and
* a prefix of the synthesizing source "source" in the zone "zone".
*
* See RFC 5155, Section 8.8 and RFC 4035, Section 5.3.4
*
* Note that if we want to prove that a.b.c.d.e.f is rightfully synthesized from a wildcard *.d.e.f, then we
* have to prove that none of the following exist:
*
* 1) a.b.c.d.e.f
* 2) *.b.c.d.e.f
* 3) b.c.d.e.f
* 4) *.c.d.e.f
* 5) c.d.e.f
*/
for (;;) {
_cleanup_free_ char *wc = NULL;
bool a = false;
/* Check if there's an NSEC or NSEC3 RR that proves that the mame we determined is really non-existing,
* i.e between the owner name and the next name of an NSEC RR. */
r = dnssec_nsec_test_enclosed(answer, DNS_TYPE_NSEC, name, zone, &a);
if (r <= 0)
return r;
authenticated = authenticated && a;
/* Strip one label off */
r = dns_name_parent(&name);
if (r <= 0)
return r;
/* Did we reach the source of synthesis? */
r = dns_name_equal(name, source);
if (r < 0)
return r;
if (r > 0) {
/* Successful exit */
*_authenticated = authenticated;
return 1;
}
/* Safety check, that the source of synthesis is still our suffix */
r = dns_name_endswith(name, source);
if (r < 0)
return r;
if (r == 0)
return -EBADMSG;
/* Replace the label we stripped off with an asterisk */
wc = strjoin("*.", name);
if (!wc)
return -ENOMEM;
/* And check if the proof holds for the asterisk name, too */
r = dnssec_nsec_test_enclosed(answer, DNS_TYPE_NSEC, wc, zone, &a);
if (r <= 0)
return r;
authenticated = authenticated && a;
/* In the next iteration we'll check the non-asterisk-prefixed version */
}
}
int dnssec_test_positive_wildcard(
DnsAnswer *answer,
const char *name,
const char *source,
const char *zone,
bool *authenticated) {
int r;
assert(name);
assert(source);
assert(zone);
assert(authenticated);
r = dns_answer_contains_zone_nsec3(answer, zone);
if (r < 0)
return r;
if (r > 0)
return dnssec_test_positive_wildcard_nsec3(answer, name, source, zone, authenticated);
else
return dnssec_test_positive_wildcard_nsec(answer, name, source, zone, authenticated);
}
#else
int dnssec_verify_rrset(
DnsAnswer *a,
const DnsResourceKey *key,
DnsResourceRecord *rrsig,
DnsResourceRecord *dnskey,
usec_t realtime,
DnssecResult *result) {
return -EOPNOTSUPP;
}
int dnssec_rrsig_match_dnskey(DnsResourceRecord *rrsig, DnsResourceRecord *dnskey, bool revoked_ok) {
return -EOPNOTSUPP;
}
int dnssec_key_match_rrsig(const DnsResourceKey *key, DnsResourceRecord *rrsig) {
return -EOPNOTSUPP;
}
int dnssec_verify_rrset_search(
DnsAnswer *a,
const DnsResourceKey *key,
DnsAnswer *validated_dnskeys,
usec_t realtime,
DnssecResult *result,
DnsResourceRecord **ret_rrsig) {
return -EOPNOTSUPP;
}
int dnssec_has_rrsig(DnsAnswer *a, const DnsResourceKey *key) {
return -EOPNOTSUPP;
}
int dnssec_verify_dnskey_by_ds(DnsResourceRecord *dnskey, DnsResourceRecord *ds, bool mask_revoke) {
return -EOPNOTSUPP;
}
int dnssec_verify_dnskey_by_ds_search(DnsResourceRecord *dnskey, DnsAnswer *validated_ds) {
return -EOPNOTSUPP;
}
int dnssec_nsec3_hash(DnsResourceRecord *nsec3, const char *name, void *ret) {
return -EOPNOTSUPP;
}
int dnssec_nsec_test(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result, bool *authenticated, uint32_t *ttl) {
return -EOPNOTSUPP;
}
int dnssec_test_positive_wildcard(
DnsAnswer *answer,
const char *name,
const char *source,
const char *zone,
bool *authenticated) {
return -EOPNOTSUPP;
}
#endif
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
static const char* const dnssec_result_table[_DNSSEC_RESULT_MAX] = {
[DNSSEC_VALIDATED] = "validated",
[DNSSEC_VALIDATED_WILDCARD] = "validated-wildcard",
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
[DNSSEC_INVALID] = "invalid",
[DNSSEC_SIGNATURE_EXPIRED] = "signature-expired",
[DNSSEC_UNSUPPORTED_ALGORITHM] = "unsupported-algorithm",
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
[DNSSEC_NO_SIGNATURE] = "no-signature",
[DNSSEC_MISSING_KEY] = "missing-key",
[DNSSEC_UNSIGNED] = "unsigned",
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
[DNSSEC_FAILED_AUXILIARY] = "failed-auxiliary",
[DNSSEC_NSEC_MISMATCH] = "nsec-mismatch",
[DNSSEC_INCOMPATIBLE_SERVER] = "incompatible-server",
resolved: chase DNSKEY/DS RRs when doing look-ups with DNSSEC enabled This adds initial support for validating RRSIG/DNSKEY/DS chains when doing lookups. Proof-of-non-existance, or proof-of-unsigned-zones is not implemented yet. With this change DnsTransaction objects will generate additional DnsTransaction objects when looking for DNSKEY or DS RRs to validate an RRSIG on a response. DnsTransaction objects are thus created for three reasons now: 1) Because a user asked for something to be resolved, i.e. requested by a DnsQuery/DnsQueryCandidate object. 2) As result of LLMNR RR probing, requested by a DnsZoneItem. 3) Because another DnsTransaction requires the requested RRs for validation of its own response. DnsTransactions are shared between all these users, and are GC automatically as soon as all of these users don't need a specific transaction anymore. To unify the handling of these three reasons for existance for a DnsTransaction, a new common naming is introduced: each DnsTransaction now tracks its "owners" via a Set* object named "notify_xyz", containing all owners to notify on completion. A new DnsTransaction state is introduced called "VALIDATING" that is entered after a response has been receieved which needs to be validated, as long as we are still waiting for the DNSKEY/DS RRs from other DnsTransactions. This patch will request the DNSKEY/DS RRs bottom-up, and then validate them top-down. Caching of RRs is now only done after verification, so that the cache is not poisoned with known invalid data. The "DnsAnswer" object gained a substantial number of new calls, since we need to add/remove RRs to it dynamically now.
2015-12-09 18:13:16 +01:00
};
DEFINE_STRING_TABLE_LOOKUP(dnssec_result, DnssecResult);
static const char* const dnssec_verdict_table[_DNSSEC_VERDICT_MAX] = {
[DNSSEC_SECURE] = "secure",
[DNSSEC_INSECURE] = "insecure",
[DNSSEC_BOGUS] = "bogus",
[DNSSEC_INDETERMINATE] = "indeterminate",
};
DEFINE_STRING_TABLE_LOOKUP(dnssec_verdict, DnssecVerdict);