/* Copyright (C) CZ.NIC, z.s.p.o. * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include #include #include #include #include #include #include #include #include #include #include #include "contrib/cleanup.h" #include "lib/defines.h" #include "lib/dnssec/nsec.h" #include "lib/dnssec/nsec3.h" #include "lib/dnssec/signature.h" #include "lib/dnssec.h" #include "lib/resolve.h" /* forward */ static int kr_rrset_validate_with_key(kr_rrset_validation_ctx_t *vctx, knot_rrset_t *covered, size_t key_pos, const struct dnssec_key *key); void kr_crypto_init(void) { dnssec_crypto_init(); } void kr_crypto_cleanup(void) { dnssec_crypto_cleanup(); } void kr_crypto_reinit(void) { dnssec_crypto_reinit(); } #define FLG_WILDCARD_EXPANSION 0x01 /**< Possibly generated by using wildcard expansion. */ /** * Check the RRSIG RR validity according to RFC4035 5.3.1 . * @param flags The flags are going to be set according to validation result. * @param cov_labels Covered RRSet owner label count. * @param rrsigs rdata containing the signatures. * @param key_alg DNSKEY's algorithm. * @param keytag Used key tag. * @param vctx->zone_name The name of the zone cut (and the DNSKEY). * @param vctx->timestamp Validation time. */ static int validate_rrsig_rr(int *flags, int cov_labels, const knot_rdata_t *rrsigs, uint8_t key_alg, uint16_t keytag, kr_rrset_validation_ctx_t *vctx) { if (kr_fails_assert(flags && rrsigs && vctx && vctx->zone_name)) { return kr_error(EINVAL); } /* bullet 5 */ if (knot_rrsig_sig_expiration(rrsigs) < vctx->timestamp) { vctx->rrs_counters.expired++; return kr_error(EINVAL); } /* bullet 6 */ if (knot_rrsig_sig_inception(rrsigs) > vctx->timestamp) { vctx->rrs_counters.notyet++; return kr_error(EINVAL); } /* bullet 2 */ const knot_dname_t *signer_name = knot_rrsig_signer_name(rrsigs); if (!signer_name || !knot_dname_is_equal(signer_name, vctx->zone_name)) { vctx->rrs_counters.signer_invalid++; return kr_error(EAGAIN); } /* bullet 4 */ { int rrsig_labels = knot_rrsig_labels(rrsigs); if (rrsig_labels > cov_labels) { vctx->rrs_counters.labels_invalid++; return kr_error(EINVAL); } if (rrsig_labels < cov_labels) { *flags |= FLG_WILDCARD_EXPANSION; } } /* bullet 7 * Part checked elsewhere: key owner matching the zone_name. */ if (key_alg != knot_rrsig_alg(rrsigs) || keytag != knot_rrsig_key_tag(rrsigs)) { vctx->rrs_counters.key_invalid++; return kr_error(EINVAL); } /* bullet 8 */ /* Checked somewhere else. */ /* bullet 9 and 10 */ /* One of the requirements should be always fulfilled. */ return kr_ok(); } /** * Returns the number of labels that have been added by wildcard expansion. * @param expanded Expanded wildcard. * @param rrsigs RRSet containing the signatures. * @param sig_pos Specifies the signature within the RRSIG RRSet. * @return Number of added labels, -1 on error. */ static inline int wildcard_radix_len_diff(const knot_dname_t *expanded, const knot_rdata_t *rrsig) { if (!expanded || !rrsig) { return -1; } return knot_dname_labels(expanded, NULL) - knot_rrsig_labels(rrsig); } int kr_rrset_validate(kr_rrset_validation_ctx_t *vctx, knot_rrset_t *covered) { if (!vctx) { return kr_error(EINVAL); } if (!vctx->pkt || !covered || !vctx->keys || !vctx->zone_name) { return kr_error(EINVAL); } memset(&vctx->rrs_counters, 0, sizeof(vctx->rrs_counters)); for (unsigned i = 0; i < vctx->keys->rrs.count; ++i) { int ret = kr_rrset_validate_with_key(vctx, covered, i, NULL); if (ret == 0 || ret == kr_error(E2BIG)) { return ret; } } return kr_error(ENOENT); } /** Assuming `rrs` was validated with `sig`, trim its TTL in case it's over-extended. */ static bool trim_ttl(knot_rrset_t *rrs, const knot_rdata_t *sig, const kr_rrset_validation_ctx_t *vctx) { /* The trimming logic is a bit complicated. * * We respect configured ttl_min over the (signed) original TTL, * but we very much want to avoid TTLs over signature expiration, * as that could cause serious issues with downstream validators. */ const uint32_t ttl_max = MIN( MAX(knot_rrsig_original_ttl(sig), vctx->ttl_min), knot_rrsig_sig_expiration(sig) - vctx->timestamp ); if (likely(rrs->ttl <= ttl_max)) return false; if (kr_log_is_debug_qry(VALIDATOR, vctx->log_qry)) { auto_free char *name_str = kr_dname_text(rrs->owner), *type_str = kr_rrtype_text(rrs->type); kr_log_q(vctx->log_qry, VALIDATOR, "trimming TTL of %s %s: %d -> %d\n", name_str, type_str, (int)rrs->ttl, (int)ttl_max); } rrs->ttl = ttl_max; return true; } typedef struct { struct dnssec_key *key; uint8_t alg; uint16_t tag; } kr_svldr_key_t; struct kr_svldr_ctx { kr_rrset_validation_ctx_t vctx; array_t(kr_svldr_key_t) keys; // owned(malloc), also insides via svldr_key_* }; static int svldr_key_new(const knot_rdata_t *rdata, const knot_dname_t *owner, kr_svldr_key_t *result) { result->alg = knot_dnskey_alg(rdata); result->key = NULL; // just silence analyzers int ret = kr_dnssec_key_from_rdata(&result->key, owner, rdata->data, rdata->len); if (likely(ret == 0)) result->tag = dnssec_key_get_keytag(result->key); return ret; } static inline void svldr_key_del(kr_svldr_key_t *skey) { kr_dnssec_key_free(&skey->key); } void kr_svldr_free_ctx(struct kr_svldr_ctx *ctx) { if (!ctx) return; for (ssize_t i = 0; i < ctx->keys.len; ++i) svldr_key_del(&ctx->keys.at[i]); array_clear(ctx->keys); free_const(ctx->vctx.zone_name); free(ctx); } struct kr_svldr_ctx * kr_svldr_new_ctx(const knot_rrset_t *ds, knot_rrset_t *dnskey, const knot_rdataset_t *dnskey_sigs, uint32_t timestamp, kr_rrset_validation_ctx_t *err_ctx) { // Basic init. struct kr_svldr_ctx *ctx = calloc(1, sizeof(*ctx)); if (unlikely(!ctx)) return NULL; ctx->vctx.timestamp = timestamp; // .ttl_min is implicitly zero ctx->vctx.zone_name = knot_dname_copy(ds->owner, NULL); if (unlikely(!ctx->vctx.zone_name)) goto fail; // Validate the DNSKEY set. ctx->vctx.keys = dnskey; if (kr_dnskeys_trusted(&ctx->vctx, dnskey_sigs, ds) != 0) goto fail; // Put usable DNSKEYs into ctx->keys. (Some duplication of work happens, but OK.) array_init(ctx->keys); array_reserve(ctx->keys, dnskey->rrs.count); knot_rdata_t *krr = dnskey->rrs.rdata; for (int i = 0; i < dnskey->rrs.count; ++i, krr = knot_rdataset_next(krr)) { if (!kr_dnssec_key_zsk(krr->data) || kr_dnssec_key_revoked(krr->data)) continue; // key not usable for this kr_svldr_key_t key; if (unlikely(svldr_key_new(krr, NULL/*seems OK here*/, &key) != 0)) goto fail; array_push(ctx->keys, key); } return ctx; fail: if (err_ctx) memcpy(err_ctx, &ctx->vctx, sizeof(*err_ctx)); kr_svldr_free_ctx(ctx); return NULL; } /** Checks whether we want to allow yet another crypto-validation and if yes, * decrements the remaining number of allowed validations. * * Returns `true` if the crypto-validation is allowed; otherwise false */ static bool account_crypto_limit(kr_rrset_validation_ctx_t *vctx) { if (vctx->limit_crypto_remains == NULL) return true; // no limiting if (*vctx->limit_crypto_remains > 0) { --*vctx->limit_crypto_remains; return true; } // We got over limit. There are optional actions to do. if (vctx->log_qry && kr_log_is_debug_qry(VALIDATOR, vctx->log_qry)) { auto_free char *name_str = kr_dname_text(vctx->zone_name); kr_log_q(vctx->log_qry, VALIDATOR, "expensive crypto limited, mitigating CVE-2023-50387, current zone: %s\n", name_str); } if (vctx->log_qry && vctx->log_qry->request) { kr_request_set_extended_error(vctx->log_qry->request, KNOT_EDNS_EDE_BOGUS, "EAIE: expensive crypto limited, mitigating CVE-2023-50387"); } return false; } static int kr_svldr_rrset_with_key(knot_rrset_t *rrs, const knot_rdataset_t *rrsigs, kr_rrset_validation_ctx_t *vctx, const kr_svldr_key_t *key) { const int covered_labels = knot_dname_labels(rrs->owner, NULL) - knot_dname_is_wildcard(rrs->owner); knot_rdata_t *rdata_j = rrsigs->rdata; for (uint16_t j = 0; j < rrsigs->count; ++j, rdata_j = knot_rdataset_next(rdata_j)) { if (kr_fails_assert(knot_rrsig_type_covered(rdata_j) == rrs->type)) continue; //^^ not a problem but no reason to allow them in the API int val_flgs = 0; int retv = validate_rrsig_rr(&val_flgs, covered_labels, rdata_j, key->alg, key->tag, vctx); if (retv == kr_error(EAGAIN)) { vctx->result = retv; return vctx->result; } else if (retv != 0) { continue; } if (!account_crypto_limit(vctx)) return vctx->result = kr_error(E2BIG); // We only expect non-expanded wildcard records in input; // that also means we don't need to perform non-existence proofs. const int trim_labels = (val_flgs & FLG_WILDCARD_EXPANSION) ? 1 : 0; if (kr_check_signature(rdata_j, key->key, rrs, trim_labels) == 0) { trim_ttl(rrs, rdata_j, vctx); vctx->result = kr_ok(); return vctx->result; } else { vctx->rrs_counters.crypto_invalid++; } } vctx->result = kr_error(ENOENT); return vctx->result; } /* The implementation basically performs "parts of" kr_rrset_validate(). */ int kr_svldr_rrset(knot_rrset_t *rrs, const knot_rdataset_t *rrsigs, struct kr_svldr_ctx *ctx) { if (knot_dname_in_bailiwick(rrs->owner, ctx->vctx.zone_name) < 0) { ctx->vctx.result = kr_error(EAGAIN); return ctx->vctx.result; } for (ssize_t i = 0; i < ctx->keys.len; ++i) { kr_svldr_rrset_with_key(rrs, rrsigs, &ctx->vctx, &ctx->keys.at[i]); if (ctx->vctx.result == 0 || ctx->vctx.result == kr_error(E2BIG)) break; } return ctx->vctx.result; } /** * Validate RRSet using a specific key. * @param vctx Pointer to validation context. * @param covered RRSet covered by a signature. It must be in canonical format. * TTL may get lowered. * @param key_pos Position of the key to be validated with. * @param key Key to be used to validate. * If NULL, then key from DNSKEY RRSet is used. * @return 0 or error code, same as vctx->result. */ static int kr_rrset_validate_with_key(kr_rrset_validation_ctx_t *vctx, knot_rrset_t *covered, size_t key_pos, const struct dnssec_key *key) { const knot_pkt_t *pkt = vctx->pkt; const knot_rrset_t *keys = vctx->keys; const knot_dname_t *zone_name = vctx->zone_name; bool has_nsec3 = vctx->has_nsec3; struct dnssec_key *created_key = NULL; if (!knot_dname_is_equal(keys->owner, zone_name) /* It's just caller's approximation that the RR is in that particular zone, * so we verify that in the following condition. * We MUST guard against attempts of zones signing out-of-bailiwick records. */ || knot_dname_in_bailiwick(covered->owner, zone_name) < 0) { vctx->result = kr_error(ENOENT); return vctx->result; } const knot_rdata_t *key_rdata = knot_rdataset_at(&keys->rrs, key_pos); if (key == NULL) { int ret = kr_dnssec_key_from_rdata(&created_key, keys->owner, key_rdata->data, key_rdata->len); if (ret != 0) { vctx->result = ret; return vctx->result; } key = created_key; } uint16_t keytag = dnssec_key_get_keytag(key); const uint8_t key_alg = knot_dnskey_alg(key_rdata); /* The asterisk does not count, RFC4034 3.1.3, paragraph 3. */ const int covered_labels = knot_dname_labels(covered->owner, NULL) - knot_dname_is_wildcard(covered->owner); for (size_t i = 0; i < vctx->rrs->len; ++i) { /* Consider every RRSIG that matches and comes from the same query. */ const knot_rrset_t *rrsig = vctx->rrs->at[i]->rr; const bool ok = vctx->rrs->at[i]->qry_uid == vctx->qry_uid && rrsig->type == KNOT_RRTYPE_RRSIG && rrsig->rclass == covered->rclass && knot_dname_is_equal(rrsig->owner, covered->owner); if (!ok) continue; knot_rdata_t *rdata_j = rrsig->rrs.rdata; for (uint16_t j = 0; j < rrsig->rrs.count; ++j, rdata_j = knot_rdataset_next(rdata_j)) { int val_flgs = 0; int trim_labels = 0; if (knot_rrsig_type_covered(rdata_j) != covered->type) { continue; } kr_rank_set(&vctx->rrs->at[i]->rank, KR_RANK_BOGUS); /* defensive style */ vctx->rrs_counters.matching_name_type++; int retv = validate_rrsig_rr(&val_flgs, covered_labels, rdata_j, key_alg, keytag, vctx); if (retv == kr_error(EAGAIN)) { vctx->result = retv; goto finish; } else if (retv != 0) { continue; } if (val_flgs & FLG_WILDCARD_EXPANSION) { trim_labels = wildcard_radix_len_diff(covered->owner, rdata_j); if (trim_labels < 0) { break; } } if (!account_crypto_limit(vctx)) { vctx->result = kr_error(E2BIG); goto finish; } if (kr_check_signature(rdata_j, key, covered, trim_labels) != 0) { vctx->rrs_counters.crypto_invalid++; continue; } if (val_flgs & FLG_WILDCARD_EXPANSION) { int ret = 0; if (!has_nsec3) { ret = kr_nsec_wildcard_answer_response_check(pkt, KNOT_AUTHORITY, covered->owner); } else { ret = kr_nsec3_wildcard_answer_response_check(pkt, KNOT_AUTHORITY, covered->owner, trim_labels - 1); if (ret == kr_error(KNOT_ERANGE)) { ret = 0; vctx->flags |= KR_DNSSEC_VFLG_OPTOUT; } } if (ret != 0) { vctx->rrs_counters.nsec_invalid++; continue; } vctx->flags |= KR_DNSSEC_VFLG_WEXPAND; } trim_ttl(covered, rdata_j, vctx); kr_rank_set(&vctx->rrs->at[i]->rank, KR_RANK_SECURE); /* upgrade from bogus */ vctx->result = kr_ok(); goto finish; } } /* No applicable key found, cannot be validated. */ vctx->result = kr_error(ENOENT); finish: kr_dnssec_key_free(&created_key); return vctx->result; } bool kr_ds_algo_support(const knot_rrset_t *ta) { if (kr_fails_assert(ta && ta->type == KNOT_RRTYPE_DS && ta->rclass == KNOT_CLASS_IN)) return false; /* Check if at least one DS has a usable algorithm pair. */ knot_rdata_t *rdata_i = ta->rrs.rdata; for (uint16_t i = 0; i < ta->rrs.count; ++i, rdata_i = knot_rdataset_next(rdata_i)) { if (dnssec_algorithm_digest_support(knot_ds_digest_type(rdata_i)) && dnssec_algorithm_key_support(knot_ds_alg(rdata_i))) { return true; } } return false; } int kr_dnskeys_trusted(kr_rrset_validation_ctx_t *vctx, const knot_rdataset_t *sigs, const knot_rrset_t *ta) { knot_rrset_t *keys = vctx->keys; const bool ok = keys && ta && ta->rrs.count && ta->rrs.rdata && ta->type == KNOT_RRTYPE_DS && knot_dname_is_equal(ta->owner, keys->owner); if (kr_fails_assert(ok)) return kr_error(EINVAL); /* RFC4035 5.2, bullet 1 * The supplied DS record has been authenticated. * It has been validated or is part of a configured trust anchor. */ knot_rdata_t *krr = keys->rrs.rdata; for (int i = 0; i < keys->rrs.count; ++i, krr = knot_rdataset_next(krr)) { /* RFC4035 5.3.1, bullet 8 */ /* ZSK */ if (!kr_dnssec_key_zsk(krr->data) || kr_dnssec_key_revoked(krr->data)) continue; kr_svldr_key_t key; if (svldr_key_new(krr, keys->owner, &key) != 0) continue; // it might e.g. be malformed int ret = kr_authenticate_referral(ta, key.key); if (ret == 0) ret = kr_svldr_rrset_with_key(keys, sigs, vctx, &key); svldr_key_del(&key); if (ret == 0 || ret == kr_error(E2BIG)) { kr_assert(vctx->result == ret); return vctx->result; } } /* No useable key found */ vctx->result = kr_error(ENOENT); return vctx->result; } bool kr_dnssec_key_zsk(const uint8_t *dnskey_rdata) { return knot_wire_read_u16(dnskey_rdata) & 0x0100; } bool kr_dnssec_key_ksk(const uint8_t *dnskey_rdata) { return knot_wire_read_u16(dnskey_rdata) & 0x0001; } /** Return true if the DNSKEY is revoked. */ bool kr_dnssec_key_revoked(const uint8_t *dnskey_rdata) { return knot_wire_read_u16(dnskey_rdata) & 0x0080; } int kr_dnssec_key_tag(uint16_t rrtype, const uint8_t *rdata, size_t rdlen) { if (!rdata || rdlen == 0 || (rrtype != KNOT_RRTYPE_DS && rrtype != KNOT_RRTYPE_DNSKEY)) { return kr_error(EINVAL); } if (rrtype == KNOT_RRTYPE_DS) { return knot_wire_read_u16(rdata); } else if (rrtype == KNOT_RRTYPE_DNSKEY) { struct dnssec_key *key = NULL; int ret = kr_dnssec_key_from_rdata(&key, NULL, rdata, rdlen); if (ret != 0) { return ret; } uint16_t keytag = dnssec_key_get_keytag(key); kr_dnssec_key_free(&key); return keytag; } else { return kr_error(EINVAL); } } int kr_dnssec_key_match(const uint8_t *key_a_rdata, size_t key_a_rdlen, const uint8_t *key_b_rdata, size_t key_b_rdlen) { dnssec_key_t *key_a = NULL, *key_b = NULL; int ret = kr_dnssec_key_from_rdata(&key_a, NULL, key_a_rdata, key_a_rdlen); if (ret != 0) { return ret; } ret = kr_dnssec_key_from_rdata(&key_b, NULL, key_b_rdata, key_b_rdlen); if (ret != 0) { dnssec_key_free(key_a); return ret; } /* If the algorithm and the public key match, we can be sure * that they are the same key. */ ret = kr_error(ENOENT); dnssec_binary_t pk_a, pk_b; if (dnssec_key_get_algorithm(key_a) == dnssec_key_get_algorithm(key_b) && dnssec_key_get_pubkey(key_a, &pk_a) == DNSSEC_EOK && dnssec_key_get_pubkey(key_b, &pk_b) == DNSSEC_EOK) { if (pk_a.size == pk_b.size && memcmp(pk_a.data, pk_b.data, pk_a.size) == 0) { ret = 0; } } dnssec_key_free(key_a); dnssec_key_free(key_b); return ret; } int kr_dnssec_key_from_rdata(struct dnssec_key **key, const knot_dname_t *kown, const uint8_t *rdata, size_t rdlen) { if (!key || !rdata || rdlen == 0) { return kr_error(EINVAL); } dnssec_key_t *new_key = NULL; const dnssec_binary_t binary_key = { .size = rdlen, .data = (uint8_t *)rdata }; int ret = dnssec_key_new(&new_key); if (ret != DNSSEC_EOK) { return kr_error(ENOMEM); } ret = dnssec_key_set_rdata(new_key, &binary_key); if (ret != DNSSEC_EOK) { dnssec_key_free(new_key); return kr_error(ret); } if (kown) { ret = dnssec_key_set_dname(new_key, kown); if (ret != DNSSEC_EOK) { dnssec_key_free(new_key); return kr_error(ENOMEM); } } *key = new_key; return kr_ok(); } void kr_dnssec_key_free(struct dnssec_key **key) { if (kr_fails_assert(key)) return; dnssec_key_free(*key); *key = NULL; } int kr_dnssec_matches_name_and_type(const ranked_rr_array_t *rrs, uint32_t qry_uid, const knot_dname_t *name, uint16_t type) { int ret = kr_error(ENOENT); for (size_t i = 0; i < rrs->len; ++i) { const ranked_rr_array_entry_t *entry = rrs->at[i]; if (kr_fails_assert(!entry->in_progress)) return kr_error(EINVAL); const knot_rrset_t *nsec = entry->rr; if (entry->qry_uid != qry_uid || entry->yielded) { continue; } if (nsec->type != KNOT_RRTYPE_NSEC && nsec->type != KNOT_RRTYPE_NSEC3) { continue; } if (!kr_rank_test(entry->rank, KR_RANK_SECURE)) { continue; } if (nsec->type == KNOT_RRTYPE_NSEC) { ret = kr_nsec_matches_name_and_type(nsec, name, type); } else { ret = kr_nsec3_matches_name_and_type(nsec, name, type); } if (ret == kr_ok()) { return kr_ok(); } else if (ret != kr_error(ENOENT)) { return ret; } } return ret; }