use alloc::vec::Vec;
use super::proof_log_yield;
use crate::{
dnssec::{Nsec3HashAlgorithm, Proof, rdata::NSEC3},
op::{Query, ResponseCode},
rr::{Name, Record, RecordType, domain::Label},
};
pub(super) fn verify_nsec3(
query: &Query,
soa_name: &Name,
response_code: ResponseCode,
answers: &[Record],
nsec3s: &[(&Name, &NSEC3)],
) -> Proof {
debug_assert!(!nsec3s.is_empty());
let nsec3s: Option<Vec<Nsec3RecordPair<'_>>> = nsec3s
.iter()
.map(|(record_name, nsec3_data)| {
split_first_label(record_name)
.filter(|(_, base)| base == soa_name)
.and_then(|(base32_hashed_name, _)| {
Some(Nsec3RecordPair {
base32_hashed_name: Label::from_raw_bytes(base32_hashed_name).ok()?,
nsec3_data,
})
})
})
.collect();
let Some(nsec3s) = nsec3s else {
return proof_log_yield(
Proof::Bogus,
query.name(),
"nsec3",
"record name format is invalid",
);
};
debug_assert!(!nsec3s.is_empty());
let first = &nsec3s[0];
let hash_algorithm = first.nsec3_data.hash_algorithm();
let salt = first.nsec3_data.salt();
let iterations = first.nsec3_data.iterations();
if nsec3s.iter().any(|r| {
r.nsec3_data.hash_algorithm() != hash_algorithm
|| r.nsec3_data.salt() != salt
|| r.nsec3_data.iterations() != iterations
}) {
return proof_log_yield(Proof::Bogus, query.name(), "nsec3", "parameter mismatch");
}
let query_name = query.name();
match response_code {
ResponseCode::NXDomain => validate_nxdomain_response(query_name, soa_name, &nsec3s),
ResponseCode::NoError => {
let wildcard_num_labels = answers.iter().find_map(|record| {
record
.data()
.as_dnssec()?
.as_rrsig()
.map(|data| data.num_labels())
});
validate_nodata_response(
query_name,
soa_name,
query.query_type(),
wildcard_num_labels,
&nsec3s,
)
}
_ => proof_log_yield(
Proof::Bogus,
query_name,
"nsec3",
&format!("unsupported response code ({response_code})")[..],
),
}
}
struct Nsec3RecordPair<'a> {
base32_hashed_name: Label,
nsec3_data: &'a NSEC3,
}
fn split_first_label(name: &Name) -> Option<(&[u8], Name)> {
let first_label = name.iter().next()?;
let base = name.base_name();
Some((first_label, base))
}
fn nsec3hash(name: &Name, salt: &[u8], iterations: u16) -> Vec<u8> {
Nsec3HashAlgorithm::SHA1
.hash(salt, name, iterations)
.unwrap()
.as_ref()
.to_vec()
}
fn hash_and_label(name: &Name, salt: &[u8], iterations: u16) -> (Vec<u8>, Label) {
let hash = nsec3hash(name, salt, iterations);
let base32_encoded = data_encoding::BASE32_DNSSEC.encode(&hash);
let label = Label::from_ascii(&base32_encoded).unwrap();
(hash, label)
}
struct HashedNameInfo {
name: Name,
hashed_name: Vec<u8>,
base32_hashed_name: Label,
}
impl HashedNameInfo {
fn new(name: Name, salt: &[u8], iterations: u16) -> Self {
let (hashed_name, base32_hashed_name) = hash_and_label(&name, salt, iterations);
Self {
name,
hashed_name,
base32_hashed_name,
}
}
}
fn find_covering_record<'a>(
nsec3s: &'a [Nsec3RecordPair<'a>],
target_hashed_name: &[u8],
target_base32_hashed_name: &Label,
) -> Option<&'a Nsec3RecordPair<'a>> {
nsec3s.iter().find(|record| {
let Some(record_next_hashed_owner_name_base32) =
record.nsec3_data.next_hashed_owner_name_base32()
else {
return false;
};
if record.base32_hashed_name < *record_next_hashed_owner_name_base32 {
record.base32_hashed_name < *target_base32_hashed_name
&& target_hashed_name < record.nsec3_data.next_hashed_owner_name()
} else {
record.base32_hashed_name > *target_base32_hashed_name
|| target_hashed_name > record.nsec3_data.next_hashed_owner_name()
}
})
}
fn validate_nxdomain_response(
query_name: &Name,
soa_name: &Name,
nsec3s: &[Nsec3RecordPair<'_>],
) -> Proof {
debug_assert!(!nsec3s.is_empty());
let salt = nsec3s[0].nsec3_data.salt();
let iterations = nsec3s[0].nsec3_data.iterations();
let (_, base32_hashed_query_name) = hash_and_label(query_name, salt, iterations);
if nsec3s
.iter()
.any(|r| r.base32_hashed_name == base32_hashed_query_name)
{
return proof_log_yield(
Proof::Bogus,
query_name,
"nsec3",
"NXDomain response with record for query name",
);
}
let (closest_encloser_proof_info, early_proof) =
closest_encloser_proof(query_name, soa_name, nsec3s);
if let Some(proof) = early_proof {
return proof_log_yield(proof, query_name, "nsec3", "returning early proof");
}
let ClosestEncloserProofInfo {
closest_encloser,
next_closer,
closest_encloser_wildcard,
} = closest_encloser_proof_info;
match (closest_encloser, next_closer, closest_encloser_wildcard) {
(Some(_), Some(_), Some(_)) => {
proof_log_yield(Proof::Secure, query_name, "nsec3", "direct proof")
}
(None, Some(_), Some(_)) if &query_name.base_name() == soa_name => proof_log_yield(
Proof::Secure,
query_name,
"nsec3",
"no direct or wildcard proof, but parent name of query is SOA",
),
_ => proof_log_yield(
Proof::Bogus,
query_name,
"nsec3",
"no proof of non-existence",
),
}
}
struct ClosestEncloserProofInfo<'a> {
closest_encloser: Option<(HashedNameInfo, &'a Nsec3RecordPair<'a>)>,
next_closer: Option<(HashedNameInfo, &'a Nsec3RecordPair<'a>)>,
closest_encloser_wildcard: Option<(HashedNameInfo, &'a Nsec3RecordPair<'a>)>,
}
fn build_encloser_candidates_list(
query_name: &Name,
soa_name: &Name,
salt: &[u8],
iterations: u16,
) -> Vec<HashedNameInfo> {
let mut candidates = Vec::with_capacity(query_name.num_labels() as usize);
let mut name = query_name.clone();
loop {
candidates.push(HashedNameInfo::new(name.clone(), salt, iterations));
if &name == soa_name {
return candidates;
}
name = name.base_name();
debug_assert_ne!(name, Name::root());
}
}
fn closest_encloser_proof<'a>(
query_name: &Name,
soa_name: &Name,
nsec3s: &'a [Nsec3RecordPair<'a>],
) -> (ClosestEncloserProofInfo<'a>, Option<Proof>) {
debug_assert!(!nsec3s.is_empty());
let salt = nsec3s[0].nsec3_data.salt();
let iterations = nsec3s[0].nsec3_data.iterations();
let mut closest_encloser_candidates =
build_encloser_candidates_list(query_name, soa_name, salt, iterations);
let closest_encloser_in_candidates = closest_encloser_candidates.iter().enumerate().find_map(
|(candidate_index, candidate_name_info)| {
let nsec3 = nsec3s
.iter()
.find(|r| r.base32_hashed_name == candidate_name_info.base32_hashed_name);
nsec3.map(|record| (candidate_index, record))
},
);
match closest_encloser_in_candidates {
Some((closest_encloser_index, closest_encloser_record)) if closest_encloser_index > 0 => {
let closest_encloser_hash_info =
closest_encloser_candidates.swap_remove(closest_encloser_index);
let closest_encloser_wildcard_name = Name::new()
.append_label("*")
.unwrap()
.append_name(&closest_encloser_hash_info.name)
.expect("closest encloser name exists in the zone");
let closest_encloser = Some((closest_encloser_hash_info, closest_encloser_record));
let next_closer_hash_info =
closest_encloser_candidates.swap_remove(closest_encloser_index - 1);
let next_closer = find_covering_record(
nsec3s,
&next_closer_hash_info.hashed_name,
&next_closer_hash_info.base32_hashed_name,
)
.map(|record| (next_closer_hash_info, record));
let wildcard_name_info =
HashedNameInfo::new(closest_encloser_wildcard_name, salt, iterations);
let wildcard = find_covering_record(
nsec3s,
&wildcard_name_info.hashed_name,
&wildcard_name_info.base32_hashed_name,
)
.map(|record| (wildcard_name_info, record));
(
ClosestEncloserProofInfo {
closest_encloser,
next_closer,
closest_encloser_wildcard: wildcard,
},
None,
)
}
Some((0, _)) => {
(
ClosestEncloserProofInfo {
closest_encloser: None,
next_closer: None,
closest_encloser_wildcard: None,
},
Some(Proof::Bogus),
)
}
Some(_) => unreachable!(
"the compiler is convinced the first two cases don't match all Some(_)s possible"
),
None if &query_name.base_name() == soa_name => {
let next_encloser_hash_info = closest_encloser_candidates.swap_remove(0);
let next_closer = find_covering_record(
nsec3s,
&next_encloser_hash_info.hashed_name,
&next_encloser_hash_info.base32_hashed_name,
)
.map(|record| (next_encloser_hash_info, record));
let closest_encloser_wildcard_name = Name::new()
.append_label("*")
.unwrap()
.append_name(soa_name)
.expect("`soa_name` is an existing domain with a valid name");
let wildcard_name_info =
HashedNameInfo::new(closest_encloser_wildcard_name, salt, iterations);
let wildcard = find_covering_record(
nsec3s,
&wildcard_name_info.hashed_name,
&wildcard_name_info.base32_hashed_name,
)
.map(|record| (wildcard_name_info, record));
(
ClosestEncloserProofInfo {
closest_encloser: None,
next_closer,
closest_encloser_wildcard: wildcard,
},
None,
)
}
None => {
(
ClosestEncloserProofInfo {
closest_encloser: None,
next_closer: None,
closest_encloser_wildcard: None,
},
Some(Proof::Bogus),
)
}
}
}
fn validate_nodata_response(
query_name: &Name,
soa_name: &Name,
query_type: RecordType,
wildcard_encloser_num_labels: Option<u8>,
nsec3s: &[Nsec3RecordPair<'_>],
) -> Proof {
debug_assert!(!nsec3s.is_empty());
let salt = nsec3s[0].nsec3_data.salt();
let iterations = nsec3s[0].nsec3_data.iterations();
let (hashed_query_name, base32_hashed_query_name) =
hash_and_label(query_name, salt, iterations);
let query_name_record = nsec3s
.iter()
.find(|record| record.base32_hashed_name == base32_hashed_query_name);
if let Some(query_record) = query_name_record {
if query_record.nsec3_data.type_set().contains(query_type)
|| query_record
.nsec3_data
.type_set()
.contains(RecordType::CNAME)
{
return proof_log_yield(
Proof::Bogus,
query_name,
"nsec3",
&format!("nsec3 type map covers {query_type} or CNAME")[..],
);
} else {
return proof_log_yield(
Proof::Secure,
query_name,
"nsec3",
&format!("type map does not cover {query_type} or CNAME")[..],
);
}
}
if query_type == RecordType::DS
&& find_covering_record(nsec3s, &hashed_query_name, &base32_hashed_query_name)
.is_some_and(|x| x.nsec3_data.opt_out())
{
return proof_log_yield(
Proof::Secure,
query_name,
"nsec3",
"DS query covered by opt-out proof",
);
}
let (proof, reason) = match wildcard_encloser_num_labels {
Some(wildcard_encloser_num_labels) => {
if query_name.num_labels() <= wildcard_encloser_num_labels {
return proof_log_yield(
Proof::Bogus,
query_name,
"nsec3",
&format!(
"query labels ({}) <= wildcard encloser labels ({})",
query_name.num_labels(),
wildcard_encloser_num_labels,
)[..],
);
}
let next_closer_labels = query_name
.into_iter()
.rev()
.take(wildcard_encloser_num_labels as usize + 1)
.rev()
.collect::<Vec<_>>();
let next_closer_name = Name::from_labels(next_closer_labels)
.expect("next closer is `query_name` or its ancestor");
let next_closer_name_info = HashedNameInfo::new(next_closer_name, salt, iterations);
let next_closer_record = find_covering_record(
nsec3s,
&next_closer_name_info.hashed_name,
&next_closer_name_info.base32_hashed_name,
);
match next_closer_record {
Some(_) => (Proof::Secure, "matching next closer record"),
None => (Proof::Bogus, "no matching next closer record"),
}
}
None => {
let ClosestEncloserProofInfo {
closest_encloser,
next_closer,
closest_encloser_wildcard,
} = wildcard_based_encloser_proof(query_name, soa_name, nsec3s);
match (closest_encloser, next_closer, closest_encloser_wildcard) {
(Some(_), Some(_), Some(_)) => (
Proof::Secure,
"servicing wildcard with closest encloser proof",
),
(None, Some(_), Some(_)) if &query_name.base_name() == soa_name => (
Proof::Secure,
"servicing wildcard without closest encloser proof, but query parent name == SOA",
),
(None, None, None) if query_name == soa_name => (
Proof::Secure,
"no servicing wildcard, but query name == SOA",
),
_ => (Proof::Bogus, "no valid servicing wildcard proof"),
}
}
};
proof_log_yield(proof, query_name, "nsec3", reason)
}
fn wildcard_based_encloser_proof<'a>(
query_name: &Name,
soa_name: &Name,
nsec3s: &'a [Nsec3RecordPair<'a>],
) -> ClosestEncloserProofInfo<'a> {
debug_assert!(!nsec3s.is_empty());
let salt = nsec3s[0].nsec3_data.salt();
let iterations = nsec3s[0].nsec3_data.iterations();
let mut closest_encloser_candidates =
build_encloser_candidates_list(query_name, soa_name, salt, iterations);
let mut wildcard_encloser_candidates = closest_encloser_candidates
.iter()
.filter(|HashedNameInfo { name, .. }| name != soa_name)
.map(|info| {
let wildcard = info.name.clone().into_wildcard();
HashedNameInfo::new(wildcard, salt, iterations)
})
.collect::<Vec<_>>();
let wildcard_encloser = wildcard_encloser_candidates
.iter()
.enumerate()
.find_map(|(index, wildcard)| {
let wildcard_nsec3 = nsec3s
.iter()
.find(|record| record.base32_hashed_name == wildcard.base32_hashed_name);
wildcard_nsec3.map(|record| (index, record))
})
.map(|(index, record)| {
let wildcard_name_info = wildcard_encloser_candidates.swap_remove(index);
(wildcard_name_info, record)
});
let Some((wildcard_encloser_name_info, _)) = &wildcard_encloser else {
return ClosestEncloserProofInfo {
closest_encloser: None,
next_closer: None,
closest_encloser_wildcard: None,
};
};
let closest_encloser_name = wildcard_encloser_name_info.name.base_name();
let closest_encloser_index = closest_encloser_candidates
.iter()
.position(|name_info| name_info.name == closest_encloser_name)
.expect("cannot fail, always > 0");
debug_assert!(closest_encloser_index >= 1);
let closest_encloser_name_info =
closest_encloser_candidates.swap_remove(closest_encloser_index);
let closest_encloser_covering_record = find_covering_record(
nsec3s,
&closest_encloser_name_info.hashed_name,
&closest_encloser_name_info.base32_hashed_name,
);
let next_closer_index = closest_encloser_index - 1;
let next_closer_name_info = closest_encloser_candidates.swap_remove(next_closer_index);
let next_closer_covering_record = find_covering_record(
nsec3s,
&next_closer_name_info.hashed_name,
&next_closer_name_info.base32_hashed_name,
);
ClosestEncloserProofInfo {
closest_encloser: closest_encloser_covering_record
.map(|record| (closest_encloser_name_info, record)),
next_closer: next_closer_covering_record.map(|record| (next_closer_name_info, record)),
closest_encloser_wildcard: wildcard_encloser,
}
}