use core::cmp::min;
use core::fmt::{Debug, Display};
use std::vec::Vec;
use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate};
use crate::base::iana::Rtype;
use crate::base::name::ToName;
use crate::base::record::Record;
use crate::dnssec::sign::error::SigningError;
use crate::dnssec::sign::records::RecordsIter;
use crate::rdata::dnssec::RtypeBitmap;
use crate::rdata::{Nsec, ZoneRecordData};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GenerateNsecConfig {
pub assume_dnskeys_will_be_added: bool,
}
impl GenerateNsecConfig {
pub fn new() -> Self {
Self {
assume_dnskeys_will_be_added: true,
}
}
pub fn without_assuming_dnskeys_will_be_added(mut self) -> Self {
self.assume_dnskeys_will_be_added = false;
self
}
}
impl Default for GenerateNsecConfig {
fn default() -> Self {
Self {
assume_dnskeys_will_be_added: true,
}
}
}
#[allow(clippy::type_complexity)]
pub fn generate_nsecs<N, Octs>(
apex_owner: &N,
mut records: RecordsIter<'_, N, ZoneRecordData<Octs, N>>,
config: &GenerateNsecConfig,
) -> Result<Vec<Record<N, Nsec<Octs, N>>>, SigningError>
where
N: ToName + Clone + Display + PartialEq,
Octs: FromBuilder,
Octs::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>,
<Octs::Builder as OctetsBuilder>::AppendError: Debug,
{
let mut nsecs = Vec::new();
let mut zone_class = None;
let mut nsec_ttl = None;
let mut cut: Option<N> = None;
let mut prev: Option<(N, RtypeBitmap<Octs>)> = None;
records.skip_before(apex_owner);
for owner_rrs in records {
if !owner_rrs.is_in_zone(apex_owner) {
break;
}
if let Some(ref cut) = cut {
if owner_rrs.owner().ends_with(cut) {
continue;
}
}
let name = owner_rrs.owner().clone();
cut = if owner_rrs.is_zone_cut(apex_owner) {
Some(name.clone())
} else {
None
};
if let Some((prev_name, bitmap)) = prev.take() {
nsecs.push(Record::new(
prev_name.clone(),
zone_class.unwrap(),
nsec_ttl.unwrap(),
Nsec::new(name.clone(), bitmap),
));
}
let mut bitmap = RtypeBitmap::<Octs>::builder();
bitmap.add(Rtype::RRSIG).unwrap();
if config.assume_dnskeys_will_be_added
&& owner_rrs.owner() == apex_owner
{
bitmap.add(Rtype::DNSKEY).unwrap();
}
bitmap.add(Rtype::NSEC).unwrap();
for rrset in owner_rrs.rrsets() {
if cut.is_none() || matches!(rrset.rtype(), Rtype::NS | Rtype::DS)
{
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);
};
nsec_ttl = Some(min(soa_data.minimum(), soa_rr.ttl()));
zone_class = Some(rrset.class());
}
}
if nsec_ttl.is_none() {
return Err(SigningError::SoaRecordCouldNotBeDetermined);
}
prev = Some((name, bitmap.finalize()));
}
if let Some((prev_name, bitmap)) = prev {
nsecs.push(Record::new(
prev_name.clone(),
zone_class.unwrap(),
nsec_ttl.unwrap(),
Nsec::new(apex_owner.clone(), bitmap),
));
}
Ok(nsecs)
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::base::{Name, Ttl};
use crate::dnssec::sign::records::SortedRecords;
use crate::dnssec::sign::test_util::*;
use crate::zonetree::types::StoredRecordData;
use crate::zonetree::StoredName;
use super::*;
use core::str::FromStr;
type StoredSortedRecords = SortedRecords<StoredName, StoredRecordData>;
#[test]
fn soa_is_required() {
let cfg = GenerateNsecConfig::default()
.without_assuming_dnskeys_will_be_added();
let apex = Name::from_str("a.").unwrap();
let records = StoredSortedRecords::from_iter([mk_a_rr("some_a.a.")]);
let res = generate_nsecs(&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 = GenerateNsecConfig::default()
.without_assuming_dnskeys_will_be_added();
let apex = Name::from_str("a.").unwrap();
let records = StoredSortedRecords::from_iter([
mk_soa_rr("a.", "b.", "c."),
mk_soa_rr("a.", "d.", "e."),
]);
let res = generate_nsecs(&apex, records.owner_rrs(), &cfg);
assert!(matches!(
res,
Err(SigningError::SoaRecordCouldNotBeDetermined)
));
}
#[test]
fn records_outside_zone_are_ignored() {
let cfg = GenerateNsecConfig::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 = StoredSortedRecords::from_iter([
mk_soa_rr("b.", "d.", "e."),
mk_a_rr("some_a.b."),
mk_soa_rr("a.", "b.", "c."),
mk_a_rr("some_a.a."),
]);
let nsecs =
generate_nsecs(&a_apex, records.owner_rrs(), &cfg).unwrap();
assert_eq!(
nsecs,
[
mk_nsec_rr("a.", "some_a.a.", "SOA RRSIG NSEC"),
mk_nsec_rr("some_a.a.", "a.", "A RRSIG NSEC"),
]
);
let nsecs =
generate_nsecs(&b_apex, records.owner_rrs(), &cfg).unwrap();
assert_eq!(
nsecs,
[
mk_nsec_rr("b.", "some_a.b.", "SOA RRSIG NSEC"),
mk_nsec_rr("some_a.b.", "b.", "A RRSIG NSEC"),
]
);
}
#[test]
fn glue_records_are_ignored() {
let cfg = GenerateNsecConfig::default()
.without_assuming_dnskeys_will_be_added();
let apex = Name::from_str("example.").unwrap();
let records = StoredSortedRecords::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 nsecs = generate_nsecs(&apex, records.owner_rrs(), &cfg).unwrap();
assert_eq!(
nsecs,
[
mk_nsec_rr(
"example.",
"in_zone.example.",
"NS SOA RRSIG NSEC"
),
mk_nsec_rr("in_zone.example.", "example.", "A RRSIG NSEC"),
]
);
}
#[test]
fn occluded_records_are_ignored() {
let cfg = GenerateNsecConfig::default()
.without_assuming_dnskeys_will_be_added();
let apex = Name::from_str("a.").unwrap();
let records = StoredSortedRecords::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 nsecs = generate_nsecs(&apex, records.owner_rrs(), &cfg).unwrap();
assert_eq!(
nsecs,
[
mk_nsec_rr("a.", "some_ns.a.", "SOA RRSIG NSEC"),
mk_nsec_rr("some_ns.a.", "a.", "NS RRSIG NSEC"),
]
);
assert!(!contains_owner(&nsecs, "some_a.some_ns.a.example."));
}
#[test]
fn expect_dnskeys_at_the_apex() {
let cfg = GenerateNsecConfig::default();
let apex = Name::from_str("a.").unwrap();
let records = StoredSortedRecords::from_iter([
mk_soa_rr("a.", "b.", "c."),
mk_a_rr("some_a.a."),
]);
let nsecs = generate_nsecs(&apex, records.owner_rrs(), &cfg).unwrap();
assert_eq!(
nsecs,
[
mk_nsec_rr("a.", "some_a.a.", "SOA DNSKEY RRSIG NSEC"),
mk_nsec_rr("some_a.a.", "a.", "A RRSIG NSEC"),
]
);
}
#[test]
fn rfc_4034_appendix_a_and_rfc_9077_compliant() {
let cfg = GenerateNsecConfig::default()
.without_assuming_dnskeys_will_be_added();
let zonefile = include_bytes!(
"../../../../test-data/zonefiles/rfc4035-appendix-A.zone"
);
let apex = Name::from_str("example.").unwrap();
let records = bytes_to_records(&zonefile[..]);
let nsecs = generate_nsecs(&apex, records.owner_rrs(), &cfg).unwrap();
assert_eq!(nsecs.len(), 10);
assert_eq!(
nsecs,
[
mk_nsec_rr("example.", "a.example.", "NS SOA MX RRSIG NSEC"),
mk_nsec_rr("a.example.", "ai.example.", "NS DS RRSIG NSEC"),
mk_nsec_rr(
"ai.example.",
"b.example",
"A HINFO AAAA RRSIG NSEC"
),
mk_nsec_rr("b.example.", "ns1.example.", "NS RRSIG NSEC"),
mk_nsec_rr("ns1.example.", "ns2.example.", "A RRSIG NSEC"),
mk_nsec_rr("ns2.example.", "*.w.example.", "A RRSIG NSEC"),
mk_nsec_rr("*.w.example.", "x.w.example.", "MX RRSIG NSEC"),
mk_nsec_rr("x.w.example.", "x.y.w.example.", "MX RRSIG NSEC"),
mk_nsec_rr("x.y.w.example.", "xx.example.", "MX RRSIG NSEC"),
mk_nsec_rr(
"xx.example.",
"example.",
"A HINFO AAAA RRSIG NSEC"
)
],
);
for nsec in &nsecs {
assert_eq!(nsec.ttl(), Ttl::from_secs(1800));
}
for nsec in &nsecs {
assert!(nsec.data().types().contains(Rtype::NSEC));
assert!(nsec.data().types().contains(Rtype::RRSIG));
}
assert!(contains_owner(&nsecs, "ns1.example."));
assert!(!contains_owner(&nsecs, "ns1.a.example."));
assert!(!contains_owner(&nsecs, "ns1.b.example."));
assert!(contains_owner(&nsecs, "ns2.example."));
assert!(!contains_owner(&nsecs, "ns2.a.example."));
let name = mk_name("b.example.");
let nsec = nsecs.iter().find(|rr| rr.owner() == &name).unwrap();
assert!(nsec.data().types().contains(Rtype::NSEC));
assert!(nsec.data().types().contains(Rtype::RRSIG));
assert!(!nsec.data().types().contains(Rtype::A));
}
#[test]
fn existing_nsec_records_are_ignored() {
let cfg = GenerateNsecConfig::default();
let apex = Name::from_str("a.").unwrap();
let records = StoredSortedRecords::from_iter([
mk_soa_rr("a.", "b.", "c."),
mk_a_rr("some_a.a."),
mk_nsec_rr("a.", "some_a.a.", "SOA NSEC"),
mk_nsec_rr("some_a.a.", "a.", "A RRSIG NSEC"),
]);
let nsecs = generate_nsecs(&apex, records.owner_rrs(), &cfg).unwrap();
assert_eq!(
nsecs,
[
mk_nsec_rr("a.", "some_a.a.", "SOA DNSKEY RRSIG NSEC"),
mk_nsec_rr("some_a.a.", "a.", "A RRSIG NSEC"),
]
);
}
}