blob: b83bd657b891ffa47789203d4c233dcc7c69cc5b [file] [log] [blame]
/*
* Copyright (c) 2022 Apple Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//======================================================================================================================
// MARK: - Headers
#include "dns_obj_log.h"
#include "dns_obj_rr_nsec3.h"
#include "dns_obj_rr_private.h"
#include "domain_name_labels.h"
#include "rdata_parser.h"
#include "base_encoding.h"
#include "dns_assert_macros.h"
#include "mdns_strict.h"
//======================================================================================================================
// MARK: - DNSSEC NSEC3 Resource Record Kind Definition
struct dns_obj_rr_nsec3_s {
struct dns_obj_rr_s base; // The reference count and kind support base.
dns_obj_domain_name_t next_hashed_owner_name; // The domain name object of the next owner name.
bool last_nsec3; // If this NSEC3 record is the last one in the zone.
};
// dns_obj_rr_nsec3_t is a subkind of dns_obj_rr_t, and it always have DNS type: kDNSRecordType_NSEC3.
DNS_OBJECT_SUBKIND_DEFINE_FULL(rr, nsec3,
.rr_type = kDNSRecordType_NSEC3,
.copy_rdata_rfc_description_method = NULL
);
//======================================================================================================================
// MARK: - DNSSEC NSEC3 Resource Record Local Prototypes
static void
dns_obj_rr_nsec3_init_fields(dns_obj_rr_nsec3_t NONNULL me, const uint8_t * NONNULL name,
const uint8_t * NONNULL rdata, dns_obj_error_t * NONNULL out_error);
static bool
dns_obj_rr_nsec3_check_hash_algorithm(dns_obj_rr_nsec3_t NONNULL me);
static bool
dns_obj_rr_nsec3_check_flags(dns_obj_rr_nsec3_t NONNULL me);
//======================================================================================================================
// MARK: - DNSSEC NSEC3 Resource Record Public Methods
dns_obj_rr_nsec3_t
dns_obj_rr_nsec3_create(const uint8_t * const name, const uint8_t * const rdata, const uint16_t rdata_len,
const bool allocate_memory, dns_obj_error_t * const out_error)
{
dns_obj_error_t err;
dns_obj_rr_nsec3_t nsec3 = NULL;
dns_obj_rr_nsec3_t obj = NULL;
const size_t name_label_count = domain_name_labels_count_label(name);
require_action(name_label_count > 1, exit, err = DNS_OBJ_ERROR_PARAM_ERR);
const bool valid = rdata_parser_nsec3_check_validity(rdata, rdata_len);
require_action(valid, exit, err = DNS_OBJ_ERROR_PARAM_ERR);
obj = _dns_obj_rr_nsec3_new();
require_action(obj != NULL, exit, err = DNS_OBJ_ERROR_NO_MEMORY);
_dns_obj_rr_nsec3_kind.dns_obj_rr_init_fields(&obj->base, name, _dns_obj_rr_nsec3_kind.rr_type,
kDNSClassType_IN, rdata, rdata_len, allocate_memory, _dns_obj_rr_nsec3_kind.copy_rdata_rfc_description_method, &err);
require_noerr(err, exit);
dns_obj_rr_nsec3_init_fields(obj, name, rdata, &err);
require_noerr(err, exit);
nsec3 = obj;
obj = NULL;
err = DNS_OBJ_ERROR_NO_ERROR;
exit:
if (out_error != NULL) {
*out_error = err;
}
MDNS_DISPOSE_DNS_OBJ(obj);
return nsec3;
}
//======================================================================================================================
dns_obj_domain_name_t
dns_obj_rr_nsec3_get_current_owner_name(const dns_obj_rr_nsec3_t me)
{
return dns_obj_rr_get_name(me);
}
//======================================================================================================================
uint8_t
dns_obj_rr_nsec3_get_hash_algorithm(const dns_obj_rr_nsec3_t me)
{
const uint8_t * const rdata = dns_obj_rr_get_rdata(me);
return rdata_parser_nsec3_get_hash_algorithm(rdata);
}
//======================================================================================================================
uint8_t
dns_obj_rr_nsec3_get_flags(const dns_obj_rr_nsec3_t me)
{
const uint8_t * const rdata = dns_obj_rr_get_rdata(me);
return rdata_parser_nsec3_get_flags(rdata);
}
//======================================================================================================================
uint16_t
dns_obj_rr_nsec3_get_iterations(const dns_obj_rr_nsec3_t me)
{
const uint8_t * const rdata = dns_obj_rr_get_rdata(me);
return rdata_parser_nsec3_get_iterations(rdata);
}
//======================================================================================================================
uint8_t
dns_obj_rr_nsec3_get_salt_length(const dns_obj_rr_nsec3_t me)
{
const uint8_t * const rdata = dns_obj_rr_get_rdata(me);
return rdata_parser_nsec3_get_salt_length(rdata);
}
//======================================================================================================================
const uint8_t *
dns_obj_rr_nsec3_get_salt(const dns_obj_rr_nsec3_t me, uint8_t * const out_salt_length)
{
const uint8_t * const rdata = dns_obj_rr_get_rdata(me);
*out_salt_length = rdata_parser_nsec3_get_salt_length(rdata);
return rdata_parser_nsec3_get_salt(rdata);
}
//======================================================================================================================
const uint8_t *
dns_obj_rr_nsec3_get_next_hashed_owner_name_in_binary(const dns_obj_rr_nsec3_t me,
uint8_t * const out_hash_length)
{
const uint8_t * const rdata = dns_obj_rr_get_rdata(me);
*out_hash_length = rdata_parser_nsec3_get_hash_length(rdata);
return rdata_parser_nsec3_get_next_hashed_owner_name(rdata);
}
//======================================================================================================================
dns_obj_domain_name_t
dns_obj_rr_nsec3_get_next_hashed_owner_name(const dns_obj_rr_nsec3_t me)
{
dns_obj_domain_name_t next_hashed_owner_name = me->next_hashed_owner_name;
return next_hashed_owner_name;
}
//======================================================================================================================
bool
dns_obj_rr_nsec3_get_opt_out_enabled(const dns_obj_rr_nsec3_t me)
{
return (dns_obj_rr_nsec3_get_flags(me) & NSEC3_FLAG_OPT_OUT) != 0;
}
//======================================================================================================================
bool
dns_obj_rr_nsec3_covers_dns_type(dns_obj_rr_nsec3_t NONNULL nsec3, const uint16_t type)
{
const uint8_t * const rdata = dns_obj_rr_get_rdata(nsec3);
const uint16_t rdata_len = dns_obj_rr_get_rdata_len(nsec3);
uint16_t type_bit_maps_len;
const uint8_t * const type_bit_maps = rdata_parser_nsec3_get_type_bit_maps(rdata, rdata_len, &type_bit_maps_len);
return rdata_parser_type_bit_maps_cover_dns_type(type_bit_maps, type_bit_maps_len, type);
}
//======================================================================================================================
bool
dns_obj_rr_nsec3_have_same_closest_parent(const dns_obj_rr_nsec3_t me, const dns_obj_rr_nsec3_t other)
{
const uint8_t * const my_labels = dns_obj_rr_get_name_in_labels(me);
const uint8_t * const others_labels = dns_obj_rr_get_name_in_labels(other);
const uint8_t * const my_parent = domain_name_labels_get_parent(my_labels, 1);
require_return_value(my_parent != NULL, false);
const uint8_t * const others_parent = domain_name_labels_get_parent(others_labels, 1);
require_return_value(others_parent != NULL, false);
const compare_result_t result = domain_name_labels_canonical_compare(my_parent, others_parent, true);
return (result == compare_result_equal);
}
//======================================================================================================================
bool
dns_obj_rr_nsec3_has_same_nsec3_parameters(const dns_obj_rr_nsec3_t me, const dns_obj_rr_nsec3_t other)
{
if (me == other) {
return true;
}
if (dns_obj_rr_nsec3_get_hash_algorithm(me) != dns_obj_rr_nsec3_get_hash_algorithm(other) ||
dns_obj_rr_nsec3_get_iterations(me) != dns_obj_rr_nsec3_get_iterations(other) ||
dns_obj_rr_nsec3_get_salt_length(me) != dns_obj_rr_nsec3_get_salt_length(other)) {
return false;
}
uint8_t my_salt_len;
const uint8_t * const my_salt = dns_obj_rr_nsec3_get_salt(me, &my_salt_len);
uint8_t others_salt_len;
const uint8_t * const others_salt = dns_obj_rr_nsec3_get_salt(other, &others_salt_len);
return memcmp(my_salt, others_salt, my_salt_len) == 0;
}
//======================================================================================================================
bool
dns_obj_rr_nsec3_has_reasonable_iterations(const dns_obj_rr_nsec3_t me)
{
// Validating resolvers SHOULD return an insecure response when processing NSEC3 records with iterations larger
// than 100.
// See https://datatracker.ietf.org/doc/draft-ietf-dnsop-nsec3-guidance/
const uint16_t max_reasonable_iterations = 100;
return dns_obj_rr_nsec3_get_iterations(me) <= max_reasonable_iterations;
}
//======================================================================================================================
bool
dns_obj_rr_nsec3_should_be_ignored(const dns_obj_rr_nsec3_t me)
{
// A validator MUST ignore NSEC3 RRs with unknown hash types.
if (!dns_obj_rr_nsec3_check_hash_algorithm(me)) {
return true;
}
// A validator MUST ignore NSEC3 RRs with a Flag fields value other than zero or one.
if (!dns_obj_rr_nsec3_check_flags(me)) {
return true;
}
return false;
}
//======================================================================================================================
bool
dns_obj_rr_nsec3_asserts_name_exists(const dns_obj_rr_nsec3_t me, const dns_obj_domain_name_t name,
const uint16_t qclass)
{
// The class has to be the same.
if (dns_obj_rr_get_class(me) != qclass) {
return false;
}
dns_obj_domain_name_t hashed_name = dns_obj_domain_name_get_nsec3_hashed_name(name, me);
require_return_value(hashed_name != NULL, false);
// The current owner name or the next owner name of the NSEC3 has to match the hashed name.
const bool equal_to_current = dns_obj_equal(dns_obj_rr_nsec3_get_current_owner_name(me), hashed_name);
const bool equal_to_next = dns_obj_equal(dns_obj_rr_nsec3_get_next_hashed_owner_name(me), hashed_name);
return (equal_to_current || equal_to_next);
}
//======================================================================================================================
bool
dns_obj_rr_nsec3_asserts_name_exists_data_does_not_exist(const dns_obj_rr_nsec3_t me,
const dns_obj_domain_name_t name, const uint16_t qclass, const uint16_t qtype)
{
// The class has to be the same.
if (dns_obj_rr_get_class(me) != qclass) {
return false;
}
// The type bit map must not cover the type.
if (dns_obj_rr_nsec3_covers_dns_type(me, qtype)) {
return false;
}
// [Check for CNAME](https://datatracker.ietf.org/doc/html/rfc6840#section-4.3)
// When validating a NOERROR/NODATA response, validators MUST check the CNAME bit in the matching NSEC or NSEC3 RR's
// type bitmap in addition to the bit for the query type.
// When we are not asking for CNAME record, if the returned NSEC3 only covers CNAME (it always covers RRSIG and NSEC3),
// this NSEC does not prove that the name we asked for has no data. We need to follow the CNAME to the end and get
// NSEC3 there to prove that.
if (dns_obj_rr_nsec3_covers_dns_type(me, kDNSRecordType_CNAME)) {
return false;
}
dns_obj_domain_name_t hashed_name = dns_obj_domain_name_get_nsec3_hashed_name(name, me);
require_return_value(hashed_name != NULL, false);
// The name of the NSEC3 has to match the hashed name.
return dns_obj_equal(dns_obj_rr_get_name(me), hashed_name);
}
//======================================================================================================================
bool
dns_obj_rr_nsec3_asserts_name_does_not_exist(const dns_obj_rr_nsec3_t me, const dns_obj_domain_name_t name,
const uint16_t qclass)
{
// The class has to be the same.
if (dns_obj_rr_get_class(me) != qclass) {
return false;
}
// The name does not exist if it appears in-between the current hashed owner name and the next hashed owner name.
const dns_obj_domain_name_t current_hashed_owner_name = dns_obj_rr_get_name(me);
const dns_obj_domain_name_t next_hashed_owner_name = dns_obj_rr_nsec3_get_next_hashed_owner_name(me);
// Get the base32 hex encoded hash name for the domain name that will be compared.
dns_obj_domain_name_t hashed_name = dns_obj_domain_name_get_nsec3_hashed_name(name, me);
require_return_value(hashed_name != NULL, false);
bool name_not_exists;
if (!me->last_nsec3) {
// If the NSEC3 record is not the last one in the zone.
name_not_exists = (dns_obj_compare(current_hashed_owner_name, hashed_name) == compare_result_less &&
dns_obj_compare(hashed_name, next_hashed_owner_name) == compare_result_less);
} else {
// If the NSEC3 record is the last one in the zone.
// If the the hashed question name appears after the current hashed owner name and appears before the next
// hashed owner name, then the question is in the gap defined by this NSEC3 record. Thus it does not exist.
name_not_exists = (dns_obj_compare(current_hashed_owner_name, hashed_name) == compare_result_less ||
dns_obj_compare(hashed_name, next_hashed_owner_name) == compare_result_less);
}
return name_not_exists;
}
//======================================================================================================================
bool
dns_obj_rr_nsec3_is_usable_for_insecure_validation(const dns_obj_rr_nsec3_t me)
{
// See <https://datatracker.ietf.org/doc/html/rfc6840#section-4.4>
// When proving a delegation is not secure, needs to check for the absence of the DS and SOA bits in the NSEC3 type bitmap.
// The validator also MUST check for the presence of the NS bit in the matching NSEC3 RR (proving that there is, indeed, a delegation).
return (!dns_obj_rr_nsec3_covers_dns_type(me, kDNSRecordType_DS) &&
!dns_obj_rr_nsec3_covers_dns_type(me, kDNSRecordType_SOA) &&
dns_obj_rr_nsec3_covers_dns_type(me, kDNSRecordType_NS));
}
//======================================================================================================================
// MARK: - DNSSEC Resource Record Private Methods
static compare_result_t
_dns_obj_rr_nsec3_compare(const dns_obj_rr_nsec3_t me, const dns_obj_rr_nsec3_t other,
const bool check_equality_only)
{
if (check_equality_only) {
// Let the comparator of the super kind to do comparison if there is one.
return compare_result_unknown;
}
const bool have_same_parent = dns_obj_rr_nsec3_have_same_closest_parent(me, other);
if (!have_same_parent) {
// If two NSEC3s have different parent, then they are not equal and cannot be compared.
return compare_result_notequal;
}
const uint8_t * const my_labels = dns_obj_rr_get_name_in_labels(me);
const uint8_t * const others_labels = dns_obj_rr_get_name_in_labels(other);
return domain_name_label_canonical_compare(my_labels, others_labels, false);
}
//======================================================================================================================
static void
_dns_obj_rr_nsec3_finalize(const dns_obj_rr_nsec3_t me)
{
MDNS_DISPOSE_DNS_OBJ(me->next_hashed_owner_name);
}
//======================================================================================================================
static void
dns_obj_rr_nsec3_init_fields(const dns_obj_rr_nsec3_t me, const uint8_t * const name, const uint8_t * const rdata,
dns_obj_error_t * const out_error)
{
dns_obj_error_t err;
uint8_t base32_hex_labels_buf[MAX_DOMAIN_NAME];
uint8_t next_hashed_owner_name_in_labels[MAX_DOMAIN_NAME];
const uint8_t hash_length = rdata_parser_nsec3_get_hash_length(rdata);
const uint8_t * const next_owner_name_in_binary = rdata_parser_nsec3_get_next_hashed_owner_name(rdata);
// Validate the length of the base 32 hex encoding string.
const size_t base32_hex_str_len = base_x_get_encoded_string_length(base_encoding_type_base32_hex_without_padding, hash_length);
require_action(base32_hex_str_len <= MAX_DOMAIN_LABEL, exit, err = DNS_OBJ_ERROR_PARAM_ERR);
// Encode the binary hash value to base 32 hex string.
base32_hex_labels_buf[0] = (uint8_t)base32_hex_str_len;
base_x_encode(base_encoding_type_base32_hex_without_padding, next_owner_name_in_binary, hash_length, (char *)base32_hex_labels_buf + 1);
// Since the next hashed owner name does not have parent domain appended, we need to get it from the current owner
// name.
const uint8_t * const current_owner_name_labels = name;
const uint8_t * const parent = domain_name_labels_get_parent(current_owner_name_labels, 1);
require_action(parent != NULL, exit, err = DNS_OBJ_ERROR_UNEXPECTED_ERR);
// Append the zone domain name labels to the base32 hex encoded next hashed owner name.
domain_name_labels_concatenate(base32_hex_labels_buf, parent, next_hashed_owner_name_in_labels,
sizeof(next_hashed_owner_name_in_labels), &err);
require_noerr(err, exit);
// Make the next hashed owner name a domain name object.
me->next_hashed_owner_name = dns_obj_domain_name_create_with_labels(next_hashed_owner_name_in_labels, true, &err);
require_noerr(err, exit);
const compare_result_t compare_result = dns_obj_compare(dns_obj_rr_get_name(me), me->next_hashed_owner_name);
const bool last_nsec3 = (compare_result == compare_result_greater || compare_result == compare_result_equal);
me->last_nsec3 = last_nsec3;
exit:
*out_error = err;
}
//======================================================================================================================
static bool
dns_obj_rr_nsec3_check_hash_algorithm(const dns_obj_rr_nsec3_t me)
{
return dns_obj_rr_nsec3_get_hash_algorithm(me) == NSEC3_HASH_ALGORITHM_SHA_1;
}
//======================================================================================================================
static bool
dns_obj_rr_nsec3_check_flags(const dns_obj_rr_nsec3_t me)
{
const uint8_t flags = dns_obj_rr_nsec3_get_flags(me);
const uint8_t other_flags = (flags & (~NSEC3_FLAG_OPT_OUT));
return (other_flags == 0);
}