| /* SPDX-License-Identifier: LGPL-2.1+ */ |
| |
| #include <stdio_ext.h> |
| |
| #if HAVE_GCRYPT |
| #include <gcrypt.h> |
| #endif |
| |
| #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); |
| } |
| |
| int dnssec_canonicalize(const char *n, char *buffer, size_t buffer_max) { |
| size_t c = 0; |
| int r; |
| |
| /* Converts the specified hostname into DNSSEC canonicalized |
| * form. */ |
| |
| if (buffer_max < 2) |
| return -ENOBUFS; |
| |
| for (;;) { |
| r = dns_label_unescape(&n, buffer, buffer_max, 0); |
| if (r < 0) |
| return r; |
| if (r == 0) |
| break; |
| |
| if (buffer_max < (size_t) r + 2) |
| return -ENOBUFS; |
| |
| /* The DNSSEC canonical form is not clear on what to |
| * do with dots appearing in labels, the way DNS-SD |
| * does it. Refuse it for now. */ |
| |
| if (memchr(buffer, '.', r)) |
| return -EINVAL; |
| |
| ascii_strlower_n(buffer, (size_t) r); |
| buffer[r] = '.'; |
| |
| buffer += r + 1; |
| c += r + 1; |
| |
| buffer_max -= r + 1; |
| } |
| |
| if (c <= 0) { |
| /* Not even a single label: this is the root domain name */ |
| |
| assert(buffer_max > 2); |
| buffer[0] = '.'; |
| buffer[1] = 0; |
| |
| return 1; |
| } |
| |
| return (int) c; |
| } |
| |
| #if HAVE_GCRYPT |
| |
| 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); |
| assert(x->wire_format); |
| assert(y); |
| assert(y->wire_format); |
| |
| m = MIN(DNS_RESOURCE_RECORD_RDATA_SIZE(x), DNS_RESOURCE_RECORD_RDATA_SIZE(y)); |
| |
| r = memcmp(DNS_RESOURCE_RECORD_RDATA(x), DNS_RESOURCE_RECORD_RDATA(y), m); |
| if (r != 0) |
| return r; |
| |
| 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); |
| } |
| |
| 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); |
| 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; |
| } |
| |
| 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 */ |
| if (inception > expiration) |
| return true; |
| |
| /* Permit a certain amount of clock skew of 10% of the valid |
| * time range. This takes inspiration from unbound's |
| * resolver. */ |
| skew = (expiration - inception) / 10; |
| if (skew > SKEW_MAX) |
| skew = SKEW_MAX; |
| |
| 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: |
| case DNSSEC_ALGORITHM_ECDSAP256SHA256: |
| return GCRY_MD_SHA256; |
| |
| 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; |
| } |
| |
| int dnssec_verify_rrset( |
| DnsAnswer *a, |
| const DnsResourceKey *key, |
| DnsResourceRecord *rrsig, |
| DnsResourceRecord *dnskey, |
| usec_t realtime, |
| DnssecResult *result) { |
| |
| 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); |
| assert(result); |
| assert(rrsig->key->type == DNS_TYPE_RRSIG); |
| assert(dnskey->key->type == DNS_TYPE_DNSKEY); |
| |
| /* Verifies that the RRSet matches the specified "key" in "a", |
| * using the signature "rrsig" and the key "dnskey". It's |
| * assumed that RRSIG and DNSKEY match. */ |
| |
| r = dnssec_rrsig_prepare(rrsig); |
| if (r == -EINVAL) { |
| *result = DNSSEC_INVALID; |
| return r; |
| } |
| if (r < 0) |
| return r; |
| |
| r = dnssec_rrsig_expired(rrsig, realtime); |
| if (r < 0) |
| return r; |
| if (r > 0) { |
| *result = DNSSEC_SIGNATURE_EXPIRED; |
| return 0; |
| } |
| |
| 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 */ |
| typesafe_qsort(list, n, rr_compare); |
| |
| f = open_memstream(&sig_data, &sig_size); |
| if (!f) |
| return -ENOMEM; |
| (void) __fsetlocking(f, FSETLOCKING_BYCALLER); |
| |
| 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; |
| } |
| |
| 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), |
| hash, hash_size, |
| rrsig, |
| dnskey); |
| break; |
| |
| case DNSSEC_ALGORITHM_ECDSAP256SHA256: |
| case DNSSEC_ALGORITHM_ECDSAP384SHA384: |
| r = dnssec_ecdsa_verify( |
| gcry_md_algo_name(md_algorithm), |
| 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 |
| } |
| 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)); |
| } |
| |
| int dnssec_verify_rrset_search( |
| DnsAnswer *a, |
| const DnsResourceKey *key, |
| DnsAnswer *validated_dnskeys, |
| usec_t realtime, |
| DnssecResult *result, |
| DnsResourceRecord **ret_rrsig) { |
| |
| 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 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; |
| |
| /* 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, false); |
| if (r < 0) |
| return r; |
| if (r == 0) |
| continue; |
| |
| /* Take the time here, if it isn't set yet, so |
| * that we do all validations with the same |
| * time. */ |
| if (realtime == USEC_INFINITY) |
| realtime = now(CLOCK_REALTIME); |
| |
| /* Yay, we found a matching RRSIG with a matching |
| * DNSKEY, awesome. Now let's verify all entries of |
| * the RRSet against the RRSIG and DNSKEY |
| * combination. */ |
| |
| r = dnssec_verify_rrset(a, key, rrsig, dnskey, realtime, &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; |
| 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) |
| *result = DNSSEC_INVALID; |
| else if (found_rrsig) |
| *result = DNSSEC_MISSING_KEY; |
| else |
| *result = DNSSEC_NO_SIGNATURE; |
| |
| if (ret_rrsig) |
| *ret_rrsig = NULL; |
| |
| return 0; |
| } |
| |
| int dnssec_has_rrsig(DnsAnswer *a, const DnsResourceKey *key) { |
| DnsResourceRecord *rr; |
| int r; |
| |
| /* 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; |
| |
| case DNSSEC_DIGEST_SHA384: |
| return GCRY_MD_SHA384; |
| |
| default: |
| return -EOPNOTSUPP; |
| } |
| } |
| |
| int dnssec_verify_dnskey_by_ds(DnsResourceRecord *dnskey, DnsResourceRecord *ds, bool mask_revoke) { |
| 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) { |
| 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; |
| 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 */ |
| 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) { |
| 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; |
| |
| /* 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. */ |
| 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 */ |
| 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 */ |
| if (r <= 0) |
| return r; |
| |
| b = dns_resource_key_name(nsec3->key); |
| r = dns_name_parent(&b); /* strip off hash */ |
| 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 |
| * that there is no proof either way. The latter is the case if a the 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 < 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); |
| |
| /* 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: |
| * |
| * 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); |
| 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 = strappend("*.", 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 |
| |
| static const char* const dnssec_result_table[_DNSSEC_RESULT_MAX] = { |
| [DNSSEC_VALIDATED] = "validated", |
| [DNSSEC_VALIDATED_WILDCARD] = "validated-wildcard", |
| [DNSSEC_INVALID] = "invalid", |
| [DNSSEC_SIGNATURE_EXPIRED] = "signature-expired", |
| [DNSSEC_UNSUPPORTED_ALGORITHM] = "unsupported-algorithm", |
| [DNSSEC_NO_SIGNATURE] = "no-signature", |
| [DNSSEC_MISSING_KEY] = "missing-key", |
| [DNSSEC_UNSIGNED] = "unsigned", |
| [DNSSEC_FAILED_AUXILIARY] = "failed-auxiliary", |
| [DNSSEC_NSEC_MISMATCH] = "nsec-mismatch", |
| [DNSSEC_INCOMPATIBLE_SERVER] = "incompatible-server", |
| }; |
| 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); |