use core::cmp::min;
use core::convert::From;
use core::fmt::{Debug, Display};
use core::marker::{PhantomData, Send};
use core::ops::Deref;
use std::hash::Hash;
use std::string::String;
use std::vec::Vec;
use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate};
use octseq::OctetsFrom;
use tracing::{debug, trace};
use crate::base::iana::{Class, Nsec3HashAlgorithm, Rtype};
use crate::base::name::{ToLabelIter, ToName};
use crate::base::{CanonicalOrd, Name, NameBuilder, Record, Ttl};
use crate::dnssec::common::{nsec3_hash, Nsec3HashError};
use crate::dnssec::sign::error::SigningError;
use crate::dnssec::sign::records::{DefaultSorter, RecordsIter, Sorter};
use crate::rdata::dnssec::{RtypeBitmap, RtypeBitmapBuilder};
use crate::rdata::nsec3::{Nsec3Salt, OwnerHash};
use crate::rdata::{Nsec3, Nsec3param, ZoneRecordData};
use crate::utils::base32;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GenerateNsec3Config<Octs, Sort>
where
Octs: AsRef<[u8]> + From<&'static [u8]>,
{
pub assume_dnskeys_will_be_added: bool,
pub params: Nsec3param<Octs>,
pub opt_out_exclude_owner_names_of_unsigned_delegations: bool,
pub nsec3param_ttl_mode: Nsec3ParamTtlMode,
_phantom: PhantomData<Sort>,
}
impl<Octs, Sort> GenerateNsec3Config<Octs, Sort>
where
Octs: AsRef<[u8]> + From<&'static [u8]>,
{
pub fn new(params: Nsec3param<Octs>) -> Self {
Self {
assume_dnskeys_will_be_added: true,
params,
nsec3param_ttl_mode: Default::default(),
opt_out_exclude_owner_names_of_unsigned_delegations: true,
_phantom: Default::default(),
}
}
pub fn with_ttl_mode(mut self, ttl_mode: Nsec3ParamTtlMode) -> Self {
self.nsec3param_ttl_mode = ttl_mode;
self
}
pub fn with_opt_out(mut self) -> Self {
self.params.set_opt_out_flag();
self
}
pub fn without_opt_out_excluding_owner_names_of_unsigned_delegations(
mut self,
) -> Self {
self.opt_out_exclude_owner_names_of_unsigned_delegations = false;
self
}
pub fn without_assuming_dnskeys_will_be_added(mut self) -> Self {
self.assume_dnskeys_will_be_added = false;
self
}
}
impl<Octs> Default for GenerateNsec3Config<Octs, DefaultSorter>
where
Octs: AsRef<[u8]> + From<&'static [u8]> + Clone + FromBuilder,
<Octs as FromBuilder>::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>,
{
fn default() -> Self {
let params = Nsec3param::default();
Self {
assume_dnskeys_will_be_added: true,
params,
nsec3param_ttl_mode: Default::default(),
opt_out_exclude_owner_names_of_unsigned_delegations: true,
_phantom: Default::default(),
}
}
}
pub fn generate_nsec3s<N, Octs, Sort>(
apex_owner: &N,
mut records: RecordsIter<'_, N, ZoneRecordData<Octs, N>>,
config: &GenerateNsec3Config<Octs, Sort>,
) -> Result<Nsec3Records<N, Octs>, SigningError>
where
N: ToName + Clone + Display + Ord + Hash + Send + From<Name<Octs>>,
Octs: FromBuilder
+ From<&'static [u8]>
+ OctetsFrom<Vec<u8>>
+ Default
+ Clone
+ Send,
Octs::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>,
<Octs::Builder as OctetsBuilder>::AppendError: Debug,
Sort: Sorter,
{
let exclude_owner_names_of_unsigned_delegations =
config.params.opt_out_flag()
&& config.opt_out_exclude_owner_names_of_unsigned_delegations;
let mut nsec3s = Vec::<Record<N, Nsec3<Octs>>>::new();
let mut ents = Vec::<N>::new();
let apex_label_count = apex_owner.iter_labels().count();
let mut last_nent_stack: Vec<N> = vec![];
let mut cut: Option<N> = None;
let mut nsec3_ttl = None;
let mut nsec3param_ttl = None;
records.skip_before(apex_owner);
for owner_rrs in records {
trace!("Owner: {}", owner_rrs.owner());
if !owner_rrs.is_in_zone(apex_owner) {
debug!(
"Stopping at owner {} as it is out of zone and assumed to trail the zone",
owner_rrs.owner()
);
break;
}
if let Some(ref cut) = cut {
if owner_rrs.owner().ends_with(cut) {
debug!(
"Excluding owner {} as it is below a zone cut",
owner_rrs.owner()
);
continue;
}
}
let name = owner_rrs.owner().clone();
cut = if owner_rrs.is_zone_cut(apex_owner) {
trace!("Zone cut detected at owner {}", owner_rrs.owner());
Some(name.clone())
} else {
None
};
let has_ds = owner_rrs.records().any(|rec| rec.rtype() == Rtype::DS);
if exclude_owner_names_of_unsigned_delegations
&& cut.is_some()
&& !has_ds
{
debug!("Excluding owner {} as it is an insecure delegation (lacks a DS RR) and opt-out is enabled",owner_rrs.owner());
continue;
}
let mut last_nent_distance_to_apex = 0;
let mut last_nent = None;
while let Some(this_last_nent) = last_nent_stack.pop() {
if name.ends_with(&this_last_nent) {
last_nent_distance_to_apex =
this_last_nent.iter_labels().count() - apex_label_count;
last_nent = Some(this_last_nent);
break;
}
}
let distance_to_root = name.iter_labels().count();
let distance_to_apex = distance_to_root - apex_label_count;
if distance_to_apex > last_nent_distance_to_apex {
trace!("Possible ENT detected at owner {}", owner_rrs.owner());
let distance = distance_to_apex - last_nent_distance_to_apex;
for n in (1..=distance - 1).rev() {
let rev_label_it = name.iter_labels().skip(n);
let mut builder = NameBuilder::<Octs::Builder>::new();
for label in rev_label_it.take(distance_to_apex - n) {
builder.append_label(label.as_slice()).unwrap();
}
let name = builder.append_origin(&apex_owner).unwrap().into();
if let Err(pos) = ents.binary_search(&name) {
debug!("Found ENT at {name}");
ents.insert(pos, name);
}
}
}
let mut bitmap = RtypeBitmap::<Octs>::builder();
if cut.is_none() || has_ds {
trace!("Adding RRSIG to the bitmap as the RRSET is authoritative (not at zone cut or has a DS RR)");
bitmap.add(Rtype::RRSIG).unwrap();
}
for rrset in owner_rrs.rrsets() {
if cut.is_none() || matches!(rrset.rtype(), Rtype::NS | Rtype::DS)
{
trace!("Adding {} to the bitmap", rrset.rtype());
bitmap.add(rrset.rtype()).unwrap();
}
if rrset.rtype() == Rtype::SOA {
if rrset.len() > 1 {
return Err(SigningError::SoaRecordCouldNotBeDetermined);
}
let soa_rr = rrset.first();
let ZoneRecordData::Soa(ref soa_data) = soa_rr.data() else {
return Err(SigningError::SoaRecordCouldNotBeDetermined);
};
nsec3_ttl = Some(min(soa_data.minimum(), soa_rr.ttl()));
nsec3param_ttl = match config.nsec3param_ttl_mode {
Nsec3ParamTtlMode::Fixed(ttl) => Some(ttl),
Nsec3ParamTtlMode::Soa => Some(soa_rr.ttl()),
Nsec3ParamTtlMode::SoaMinimum => Some(soa_data.minimum()),
};
}
}
if nsec3_ttl.is_none() {
return Err(SigningError::SoaRecordCouldNotBeDetermined);
}
if distance_to_apex == 0 {
trace!("Adding NSEC3PARAM to the bitmap as we are at the apex and RRSIG RRs are expected to be added");
bitmap.add(Rtype::NSEC3PARAM).unwrap();
if config.assume_dnskeys_will_be_added {
trace!("Adding DNSKEY to the bitmap as we are at the apex and DNSKEY RRs are expected to be added");
bitmap.add(Rtype::DNSKEY).unwrap();
}
}
let rec: Record<N, Nsec3<Octs>> = mk_nsec3(
&name,
config.params.hash_algorithm(),
config.params.flags(),
config.params.iterations(),
config.params.salt(),
apex_owner,
bitmap,
nsec3_ttl.unwrap(),
)?;
nsec3s.push(rec);
if let Some(last_nent) = last_nent {
last_nent_stack.push(last_nent);
}
last_nent_stack.push(name.clone());
}
let Some(nsec3param_ttl) = nsec3param_ttl else {
return Err(SigningError::SoaRecordCouldNotBeDetermined);
};
for name in ents {
let bitmap = RtypeBitmap::<Octs>::builder();
debug!("Generating NSEC3 RR for ENT at {name}");
let rec = mk_nsec3(
&name,
config.params.hash_algorithm(),
config.params.flags(),
config.params.iterations(),
config.params.salt(),
apex_owner,
bitmap,
nsec3_ttl.unwrap(),
)?;
nsec3s.push(rec);
}
trace!("Sorting NSEC3 RRs");
Sort::sort_by(&mut nsec3s, CanonicalOrd::canonical_cmp);
nsec3s.dedup();
let only_one_nsec3 = nsec3s.len() == 1;
let first = nsec3s.first().unwrap().clone();
let mut iter = nsec3s.iter_mut().peekable();
while let Some(nsec3) = iter.next() {
let next_nsec3 = if let Some(next) = iter.peek() {
next.deref()
} else {
&first
};
if !only_one_nsec3 && nsec3.owner() == next_nsec3.owner() {
if nsec3.data().next_owner() != next_nsec3.data().next_owner() {
Err(Nsec3HashError::CollisionDetected)?;
} else {
unreachable!("All RTYPEs for a single owner name should have been combined into a single NSEC3 RR. Was the input NSEC3 canonically ordered?");
}
}
let next_owner_name: Name<Octs> = next_nsec3
.owner()
.try_to_name()
.map_err(|_| Nsec3HashError::AppendError)?;
let first_label_of_next_owner_name =
next_owner_name.iter_labels().next().unwrap();
let next_hashed_owner_name = if let Ok(hash_octets) =
base32::decode_hex(&format!("{first_label_of_next_owner_name}"))
{
OwnerHash::<Octs>::from_octets(hash_octets)
.map_err(|_| Nsec3HashError::OwnerHashError)?
} else {
return Err(Nsec3HashError::OwnerHashError)?;
};
nsec3.data_mut().set_next_owner(next_hashed_owner_name);
}
let nsec3param = Record::new(
apex_owner
.try_to_name::<Octs>()
.map_err(|_| Nsec3HashError::AppendError)?
.into(),
Class::IN,
nsec3param_ttl,
config.params.clone(),
);
Ok(Nsec3Records::new(nsec3s, nsec3param))
}
#[allow(clippy::too_many_arguments)]
fn mk_nsec3<N, Octs>(
name: &N,
alg: Nsec3HashAlgorithm,
flags: u8,
iterations: u16,
salt: &Nsec3Salt<Octs>,
apex_owner: &N,
bitmap: RtypeBitmapBuilder<<Octs as FromBuilder>::Builder>,
ttl: Ttl,
) -> Result<Record<N, Nsec3<Octs>>, Nsec3HashError>
where
N: ToName + From<Name<Octs>>,
Octs: FromBuilder + Clone + Default,
<Octs as FromBuilder>::Builder:
EmptyBuilder + AsRef<[u8]> + AsMut<[u8]> + Truncate,
{
let owner_name =
mk_hashed_nsec3_owner_name(name, alg, iterations, salt, apex_owner)?;
let placeholder_next_owner = OwnerHash::<Octs>::from_octets(
name.try_to_name::<Octs>()
.map_err(|_| Nsec3HashError::AppendError)?
.as_octets()
.clone(),
)
.map_err(|_| Nsec3HashError::OwnerHashError)?;
let nsec3 = Nsec3::new(
alg,
flags,
iterations,
salt.clone(),
placeholder_next_owner,
bitmap.finalize(),
);
Ok(Record::new(owner_name, Class::IN, ttl, nsec3))
}
pub fn mk_hashed_nsec3_owner_name<N, Octs, SaltOcts>(
name: &N,
alg: Nsec3HashAlgorithm,
iterations: u16,
salt: &Nsec3Salt<SaltOcts>,
apex_owner: &N,
) -> Result<N, Nsec3HashError>
where
N: ToName + From<Name<Octs>>,
Octs: FromBuilder,
<Octs as FromBuilder>::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>,
SaltOcts: AsRef<[u8]>,
{
let base32hex_label =
mk_base32hex_label_for_name(name, alg, iterations, salt)?;
#[cfg(test)]
if tests::NSEC3_TEST_MODE
.with(|n| *n.borrow() == tests::Nsec3TestMode::NoHash)
{
let name = N::from(name.try_to_name().ok().unwrap());
return Ok(name);
}
Ok(append_origin(base32hex_label, apex_owner))
}
fn append_origin<N, Octs>(base32hex_label: String, apex_owner: &N) -> N
where
N: ToName + From<Name<Octs>>,
Octs: FromBuilder,
<Octs as FromBuilder>::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>,
{
let mut builder = NameBuilder::<Octs::Builder>::new();
builder.append_label(base32hex_label.as_bytes()).unwrap();
let owner_name = builder.append_origin(apex_owner).unwrap();
let owner_name: N = owner_name.into();
owner_name
}
fn mk_base32hex_label_for_name<N, SaltOcts>(
name: &N,
alg: Nsec3HashAlgorithm,
iterations: u16,
salt: &Nsec3Salt<SaltOcts>,
) -> Result<String, Nsec3HashError>
where
N: ToName,
SaltOcts: AsRef<[u8]>,
{
let hash_octets: Vec<u8> =
nsec3_hash(name, alg, iterations, salt)?.into_octets();
#[cfg(test)]
let hash_octets = if tests::NSEC3_TEST_MODE
.with(|n| *n.borrow() == tests::Nsec3TestMode::Colliding)
{
vec![0; hash_octets.len()]
} else {
hash_octets
};
Ok(base32::encode_string_hex(&hash_octets).to_ascii_lowercase())
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum Nsec3ParamTtlMode {
Fixed(Ttl),
#[default]
Soa,
SoaMinimum,
}
impl Nsec3ParamTtlMode {
pub fn fixed(ttl: Ttl) -> Self {
Self::Fixed(ttl)
}
pub fn soa() -> Self {
Self::Soa
}
pub fn soa_minimum() -> Self {
Self::SoaMinimum
}
}
pub struct Nsec3Records<N, Octets> {
pub nsec3s: Vec<Record<N, Nsec3<Octets>>>,
pub nsec3param: Record<N, Nsec3param<Octets>>,
}
impl<N, Octets> Nsec3Records<N, Octets> {
pub fn new(
nsec3s: Vec<Record<N, Nsec3<Octets>>>,
nsec3param: Record<N, Nsec3param<Octets>>,
) -> Self {
Self { nsec3s, nsec3param }
}
}
#[cfg(test)]
mod tests {
use core::str::FromStr;
use std::cell::RefCell;
use pretty_assertions::assert_eq;
use crate::dnssec::sign::records::SortedRecords;
use crate::dnssec::sign::test_util::*;
use super::*;
#[derive(PartialEq)]
pub(super) enum Nsec3TestMode {
Normal,
Colliding,
NoHash,
}
thread_local! {
pub(super) static NSEC3_TEST_MODE: RefCell<Nsec3TestMode> = const { RefCell::new(Nsec3TestMode::Normal) };
}
#[test]
fn soa_is_required() {
let cfg = GenerateNsec3Config::default()
.without_assuming_dnskeys_will_be_added();
let apex = Name::from_str("a.").unwrap();
let records =
SortedRecords::<_, _>::from_iter([mk_a_rr("some_a.a.")]);
let res = generate_nsec3s(&apex, records.owner_rrs(), &cfg);
assert!(matches!(
res,
Err(SigningError::SoaRecordCouldNotBeDetermined)
));
}
#[test]
fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() {
let cfg = GenerateNsec3Config::default()
.without_assuming_dnskeys_will_be_added();
let apex = Name::from_str("a.").unwrap();
let records = SortedRecords::<_, _>::from_iter([
mk_soa_rr("a.", "b.", "c."),
mk_soa_rr("a.", "d.", "e."),
]);
let res = generate_nsec3s(&apex, records.owner_rrs(), &cfg);
assert!(matches!(
res,
Err(SigningError::SoaRecordCouldNotBeDetermined)
));
}
#[test]
fn records_outside_zone_are_ignored() {
let cfg = GenerateNsec3Config::default()
.without_assuming_dnskeys_will_be_added();
let a_apex = Name::from_str("a.").unwrap();
let b_apex = Name::from_str("b.").unwrap();
let records = SortedRecords::<_, _>::from_iter([
mk_soa_rr("b.", "d.", "e."),
mk_soa_rr("a.", "b.", "c."),
mk_a_rr("some_a.a."),
mk_a_rr("some_a.b."),
]);
let generated_records =
generate_nsec3s(&a_apex, records.owner_rrs(), &cfg).unwrap();
let expected_records = SortedRecords::<_, _>::from_iter([
mk_nsec3_rr(
"a.",
"a.",
"some_a.a.",
"SOA RRSIG NSEC3PARAM",
&cfg,
),
mk_nsec3_rr("a.", "some_a.a.", "a.", "A RRSIG", &cfg),
]);
assert_eq!(generated_records.nsec3s, expected_records.into_inner());
let generated_records =
generate_nsec3s(&b_apex, records.owner_rrs(), &cfg).unwrap();
let expected_records = SortedRecords::<_, _>::from_iter([
mk_nsec3_rr(
"b.",
"b.",
"some_a.b.",
"SOA RRSIG NSEC3PARAM",
&cfg,
),
mk_nsec3_rr("b.", "some_a.b.", "b.", "A RRSIG", &cfg),
]);
assert_eq!(generated_records.nsec3s, expected_records.into_inner());
assert!(!generated_records.nsec3param.data().opt_out_flag());
}
#[test]
fn glue_records_are_ignored() {
let cfg = GenerateNsec3Config::default()
.without_assuming_dnskeys_will_be_added();
let apex = Name::from_str("example.").unwrap();
let records = SortedRecords::<_, _>::from_iter([
mk_soa_rr("example.", "mname.", "rname."),
mk_ns_rr("example.", "early_sorting_glue."),
mk_ns_rr("example.", "late_sorting_glue."),
mk_a_rr("in_zone.example."),
mk_a_rr("early_sorting_glue."),
mk_a_rr("late_sorting_glue."),
]);
let generated_records =
generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap();
let expected_records = SortedRecords::<_, _>::from_iter([
mk_nsec3_rr(
"example.",
"example.",
"in_zone.example.",
"NS SOA RRSIG NSEC3PARAM",
&cfg,
),
mk_nsec3_rr(
"example.",
"in_zone.example.",
"example.",
"A RRSIG",
&cfg,
),
]);
assert_eq!(generated_records.nsec3s, expected_records.into_inner());
}
#[test]
fn occluded_records_are_ignored() {
let cfg = GenerateNsec3Config::default()
.without_assuming_dnskeys_will_be_added();
let apex = Name::from_str("a.").unwrap();
let records = SortedRecords::<_, _>::from_iter([
mk_soa_rr("a.", "b.", "c."),
mk_ns_rr("some_ns.a.", "some_a.other.b."),
mk_a_rr("some_a.some_ns.a."),
]);
let generated_records =
generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap();
let expected_records = SortedRecords::<_, _>::from_iter([
mk_nsec3_rr(
"a.",
"a.",
"some_ns.a.",
"SOA RRSIG NSEC3PARAM",
&cfg,
),
mk_nsec3_rr("a.", "some_ns.a.", "a.", "NS", &cfg),
]);
assert_eq!(generated_records.nsec3s, expected_records.into_inner());
assert!(!generated_records.nsec3param.data().opt_out_flag());
}
#[test]
fn expect_dnskeys_at_the_apex() {
let cfg = GenerateNsec3Config::default();
let apex = Name::from_str("a.").unwrap();
let records = SortedRecords::<_, _>::from_iter([
mk_soa_rr("a.", "b.", "c."),
mk_a_rr("some_a.a."),
]);
let generated_records =
generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap();
let expected_records = SortedRecords::<_, _>::from_iter([
mk_nsec3_rr(
"a.",
"a.",
"some_a.a.",
"SOA DNSKEY RRSIG NSEC3PARAM",
&cfg,
),
mk_nsec3_rr("a.", "some_a.a.", "a.", "A RRSIG", &cfg),
]);
assert_eq!(generated_records.nsec3s, expected_records.into_inner());
assert!(!generated_records.nsec3param.data().opt_out_flag());
}
#[test]
fn rfc_5155_appendix_a_and_rfc_9077_compliant_plus_ents() {
let nsec3params = Nsec3param::new(
Nsec3HashAlgorithm::SHA1,
1, 12,
Nsec3Salt::from_str("aabbccdd").unwrap(),
);
let cfg =
GenerateNsec3Config::<_, DefaultSorter>::new(nsec3params.clone())
.without_assuming_dnskeys_will_be_added();
let zonefile = include_bytes!(
"../../../../test-data/zonefiles/rfc5155-appendix-A.zone"
);
let apex = Name::from_str("example.").unwrap();
let records = bytes_to_records(&zonefile[..]);
let generated_records =
generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap();
let expected_records = SortedRecords::<_, _>::from_iter([
mk_precalculated_nsec3_rr(
"0p9mhaveqvm6t7vbl5lop2u3t2rp3tom.example.",
"2t7b4g4vsa5smi47k61mv5bv1a22bojr",
"NS SOA MX RRSIG NSEC3PARAM",
&cfg,
),
mk_precalculated_nsec3_rr(
"2t7b4g4vsa5smi47k61mv5bv1a22bojr.example.",
"2vptu5timamqttgl4luu9kg21e0aor3s",
"A RRSIG",
&cfg,
),
mk_precalculated_nsec3_rr(
"2vptu5timamqttgl4luu9kg21e0aor3s.example.",
"35mthgpgcu1qg68fab165klnsnk3dpvl",
"MX RRSIG",
&cfg,
),
mk_precalculated_nsec3_rr(
"35mthgpgcu1qg68fab165klnsnk3dpvl.example.",
"b4um86eghhds6nea196smvmlo4ors995",
"NS DS RRSIG",
&cfg,
),
mk_precalculated_nsec3_rr(
"b4um86eghhds6nea196smvmlo4ors995.example.",
"gjeqe526plbf1g8mklp59enfd789njgi",
"MX RRSIG",
&cfg,
),
mk_precalculated_nsec3_rr(
"gjeqe526plbf1g8mklp59enfd789njgi.example.",
"ji6neoaepv8b5o6k4ev33abha8ht9fgc",
"A HINFO AAAA RRSIG",
&cfg,
),
mk_precalculated_nsec3_rr(
"ji6neoaepv8b5o6k4ev33abha8ht9fgc.example.",
"k8udemvp1j2f7eg6jebps17vp3n8i58h",
"",
&cfg,
),
mk_precalculated_nsec3_rr(
"k8udemvp1j2f7eg6jebps17vp3n8i58h.example.",
"q04jkcevqvmu85r014c7dkba38o0ji5r",
"",
&cfg,
),
mk_precalculated_nsec3_rr(
"q04jkcevqvmu85r014c7dkba38o0ji5r.example.",
"r53bq7cc2uvmubfu5ocmm6pers9tk9en",
"A RRSIG",
&cfg,
),
mk_precalculated_nsec3_rr(
"r53bq7cc2uvmubfu5ocmm6pers9tk9en.example.",
"t644ebqk9bibcna874givr6joj62mlhv",
"MX RRSIG",
&cfg,
),
mk_precalculated_nsec3_rr(
"t644ebqk9bibcna874givr6joj62mlhv.example.",
"0p9mhaveqvm6t7vbl5lop2u3t2rp3tom",
"A HINFO AAAA RRSIG",
&cfg,
),
]);
assert_eq!(generated_records.nsec3s, expected_records.into_inner());
let expected_nsec3param = mk_nsec3param_rr("example.", &cfg);
assert_eq!(generated_records.nsec3param, expected_nsec3param);
assert!(generated_records.nsec3param.data().opt_out_flag());
for nsec3 in &generated_records.nsec3s {
assert_eq!(nsec3.ttl(), Ttl::from_secs(1800));
}
}
#[test]
fn opt_out_with_exclusion() {
let cfg = GenerateNsec3Config::default()
.with_opt_out()
.without_assuming_dnskeys_will_be_added();
let apex = Name::from_str("a.").unwrap();
let records = SortedRecords::<_, _>::from_iter([
mk_soa_rr("a.", "b.", "c."),
mk_ns_rr("unsigned_delegation.a.", "some.other.zone."),
]);
let generated_records =
generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap();
let expected_records =
SortedRecords::<_, _>::from_iter([mk_nsec3_rr(
"a.",
"a.",
"a.",
"SOA RRSIG NSEC3PARAM",
&cfg,
)]);
assert_eq!(generated_records.nsec3s, expected_records.into_inner());
assert!(generated_records.nsec3param.data().opt_out_flag());
}
#[test]
fn opt_out_without_exclusion() {
let cfg = GenerateNsec3Config::default()
.with_opt_out()
.without_opt_out_excluding_owner_names_of_unsigned_delegations()
.without_assuming_dnskeys_will_be_added();
let apex = Name::from_str("a.").unwrap();
let records = SortedRecords::<_, _>::from_iter([
mk_soa_rr("a.", "b.", "c."),
mk_ns_rr("unsigned_delegation.a.", "some.other.zone."),
]);
let generated_records =
generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap();
let expected_records = SortedRecords::<_, _>::from_iter([
mk_nsec3_rr(
"a.",
"a.",
"unsigned_delegation.a.",
"SOA RRSIG NSEC3PARAM",
&cfg,
),
mk_nsec3_rr("a.", "unsigned_delegation.a.", "a.", "NS", &cfg),
]);
assert_eq!(generated_records.nsec3s, expected_records.into_inner());
assert!(generated_records.nsec3param.data().opt_out_flag());
}
#[test]
#[should_panic(
expected = "All RTYPEs for a single owner name should have been combined into a single NSEC3 RR. Was the input NSEC3 canonically ordered?"
)]
fn generating_nsec3s_for_unordered_input_should_panic() {
let cfg = GenerateNsec3Config::default()
.without_assuming_dnskeys_will_be_added();
let apex = Name::from_str("a.").unwrap();
let records = vec![
mk_soa_rr("a.", "b.", "c."),
mk_a_rr("some_a.a."),
mk_a_rr("some_b.a."),
mk_aaaa_rr("some_a.a."),
];
let _res = generate_nsec3s(
&apex,
RecordsIter::new_from_owned(&records),
&cfg,
);
}
#[test]
fn test_nsec3_hash_collision_handling() {
let cfg = GenerateNsec3Config::<_, DefaultSorter>::new(
Nsec3param::default(),
);
NSEC3_TEST_MODE.replace(Nsec3TestMode::Colliding);
let apex = Name::from_str("a.").unwrap();
let records = SortedRecords::<_, _>::from_iter([
mk_soa_rr("a.", "b.", "c."),
mk_a_rr("some_a.a."),
]);
assert!(matches!(
generate_nsec3s(&apex, records.owner_rrs(), &cfg),
Err(SigningError::Nsec3HashingError(
Nsec3HashError::CollisionDetected
))
));
}
#[test]
fn test_nsec3_hashing_failure() {
let cfg = GenerateNsec3Config::<_, DefaultSorter>::new(
Nsec3param::default(),
);
NSEC3_TEST_MODE.replace(Nsec3TestMode::NoHash);
let apex = Name::from_str("a.").unwrap();
let records = SortedRecords::<_, _>::from_iter([
mk_soa_rr("a.", "b.", "c."),
mk_a_rr("some_a.a."),
]);
assert!(matches!(
generate_nsec3s(&apex, records.owner_rrs(), &cfg),
Err(SigningError::Nsec3HashingError(
Nsec3HashError::OwnerHashError
))
));
}
}