From ef2d2fd3054b193a364661d7812abc202e9cbd53 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Tue, 13 Jan 2026 09:54:07 +0100 Subject: [PATCH 01/20] Initial incremental signing. --- src/commands/signer.rs | 469 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 469 insertions(+) diff --git a/src/commands/signer.rs b/src/commands/signer.rs index 339bcd12..d3988572 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -10,6 +10,9 @@ use core::clone::Clone; use core::cmp::Ordering; use core::fmt::Write; use core::str::FromStr; +use domain::rdata::dnssec::RtypeBitmap; +use domain::rdata::Nsec; +use domain::dep::octseq::OctetsFrom; use domain::base::iana::nsec3::Nsec3HashAlgorithm; use domain::base::iana::zonemd::{ZonemdAlgorithm, ZonemdScheme}; use domain::base::iana::Class; @@ -44,6 +47,8 @@ use octseq::builder::with_infallible; use rayon::slice::ParallelSliceMut; use ring::digest; use serde::{Deserialize, Serialize}; +use std::io::stdout; +use std::collections::BTreeMap; use std::cmp::min; use std::collections::{HashMap, HashSet}; use std::fmt::{self, Display}; @@ -127,6 +132,7 @@ enum Commands { #[arg(long, action)] use_yyyymmddhhmmss_rrsig_format: bool, }, + Resign, Show, Cron, @@ -435,6 +441,9 @@ impl Signer { state_changed = true; res = self.go_further(env, &sc, &mut signer_state, &kss, options) } + Commands::Resign => { + self.resign(&sc, &kss, env) + } Commands::Show => { todo!(); } @@ -1138,6 +1147,44 @@ impl Signer { Ok(()) } + fn resign(&self, sc: &SignerConfig, kss: &KeySetState, _env: impl Env) { + let origin = kss.keyset.name(); + let origin_bytes = Name::::octets_from(origin.clone()); + let mut iss = IncrementalSigningState::new(origin_bytes); + load_signed_zone(&mut iss, &sc.zonefile_out).unwrap(); + + // Should check if the NSEC(3) mode has changed. Sign the full + // zone if that has happened. + + load_unsigned_zone(&mut iss, &sc.zonefile_in).unwrap(); + + initial_diffs(&mut iss); + + // Should check whether to do NSEC, NSEC3, or NSEC3 opt-out. Just do + // NSEC for now. + incremental_nsec(&mut iss); + + let mut writer = stdout(); + for (_, data) in iss.new_data { + for rr in data { + writer.write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))); + } + } + for (_, rr) in iss.nsecs { + writer.write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))); + } + for (_, data) in iss.rrsigs { + for rr in data { + let ZoneRecordData::Rrsig(rrsig) = rr.data() else { + panic!("RRSIG expected"); + }; + let rr = Record::new(rr.owner(), rr.class(), rr.ttl(), YyyyMmDdHhMMSsRrsig(rrsig)); + writer.write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))); + } + } + todo!(); + } + fn write_rr>( &self, writer: &mut W, @@ -1569,6 +1616,428 @@ struct SignerState { previous_serial: Option, } +type RtypeSet = HashSet; +type ChangesValue = (RtypeSet, RtypeSet); // add set followed by delete set. + +struct IncrementalSigningState { + origin: Name, + old_data: HashMap<(Name, Rtype), Vec>, + new_data: HashMap<(Name, Rtype), Vec>, + nsecs: BTreeMap, ZRD>, + rrsigs: HashMap<(Name, Rtype), Vec>, + + changes: HashMap, ChangesValue>, + modified_nsecs: HashSet>, +} + +impl IncrementalSigningState { + fn new(origin: Name) -> Self { + Self { + origin, + old_data: HashMap::new(), + new_data: HashMap::new(), + nsecs: BTreeMap::new(), + rrsigs: HashMap::new(), + changes: HashMap::new(), + modified_nsecs: HashSet::new(), + } + } +} + +type ZRD = Record, ZoneRecordData>>; + +fn load_signed_zone(iss: &mut IncrementalSigningState, path: &PathBuf, ) -> Result<(), Error> { + // Don't use Zonefile::load() as it knows nothing about the size of + // the original file so uses default allocation which allocates more + // bytes than are needed. Instead control the allocation size based on + // our knowledge of the file size. + let mut zone_file = File::open(path) + .map_err(|e| format!("open failed: {e}").into()) + .context(&format!( + "loading zone file from path '{}'", + path.display(), + ))?; + let zone_file_len = zone_file + .metadata() + .map_err(|e| { + format!( + "unable to get metadata from {}: {e}", + path.display() + ) + })? + .len(); + let mut buf = inplace::Zonefile::with_capacity(zone_file_len as usize).writer(); + std::io::copy(&mut zone_file, &mut buf) + .map_err(|e| format!("copy to {} failed: {e}", path.display()))?; + let mut reader = buf.into_inner(); + + reader.set_origin(iss.origin.clone()); + + // Assume the signed zone is mostly sorted. Collect records for a + // name/RRtype and store a complete RRset in a hash table. + let mut records = Vec::, ZoneRecordData>>>::new(); + let mut rrsig_records = vec![]; + let mut type_covered = Rtype::RRSIG; + + for entry in reader { + let entry = entry.map_err(|err| format!("Invalid zone file: {err}"))?; + match entry { + Entry::Record(record) => { + let record: StoredRecord = record.flatten_into(); + + match record.data() { + ZoneRecordData::Rrsig(rrsig) => { + if rrsig_records.is_empty() { + type_covered = rrsig.type_covered(); + rrsig_records.push(record); + continue; + } + if record.owner() == rrsig_records[0].owner() && + rrsig.type_covered() == type_covered { + todo!(); + } + + let key = (rrsig_records[0].owner().clone(), type_covered); + if let Some(v) = iss.rrsigs.get_mut(&key) { + v.append(&mut rrsig_records); + } else + { + iss.rrsigs.insert(key, rrsig_records); + } + type_covered = rrsig.type_covered(); + rrsig_records = vec![]; + rrsig_records.push(record); + } + ZoneRecordData::Nsec(_) => { + // Assume (at most) one NSEC record per owner name. + // Directly insert into the hash map. + iss.nsecs.insert(record.owner().clone(), record); + } + ZoneRecordData::Nsec3(_) => todo!(), + _ => { + if records.is_empty() { + records.push(record); + continue; + } + if record.owner() == records[0].owner() && + record.rtype() == records[0].rtype() { + records.push(record); + continue; + } + let key = (records[0].owner().clone(), records[0].rtype()); + if let Some(v) = iss.old_data.get_mut(&key) { + v.append(&mut records); + } else + { + iss.old_data.insert(key, records); + } + records = vec![]; + records.push(record); + } + } + } + Entry::Include { .. } => { + return Err(Error::from( + "Invalid zone file: $INCLUDE directive is not supported", + )); + } + } + } + + if !records.is_empty() { + let key = (records[0].owner().clone(), records[0].rtype()); + if let Some(v) = iss.old_data.get_mut(&key) { + v.append(&mut records); + } else + { + iss.old_data.insert(key, records); + } + } + if !rrsig_records.is_empty() { + let key = (rrsig_records[0].owner().clone(), type_covered); + if let Some(v) = iss.rrsigs.get_mut(&key) { + v.append(&mut rrsig_records); + } else + { + iss.rrsigs.insert(key, rrsig_records); + } + } + Ok(()) +} + +fn load_unsigned_zone(iss: &mut IncrementalSigningState, path: &PathBuf, ) -> Result<(), Error> { +// Basically a copy of load_signed_zone execpt that signature and NSEC(3) +// records are removed. Make sure to delete and update APEX records. + + // Don't use Zonefile::load() as it knows nothing about the size of + // the original file so uses default allocation which allocates more + // bytes than are needed. Instead control the allocation size based on + // our knowledge of the file size. + let mut zone_file = File::open(path) + .map_err(|e| format!("open failed: {e}").into()) + .context(&format!( + "loading zone file from path '{}'", + path.display(), + ))?; + let zone_file_len = zone_file + .metadata() + .map_err(|e| { + format!( + "unable to get metadata from {}: {e}", + path.display() + ) + })? + .len(); + let mut buf = inplace::Zonefile::with_capacity(zone_file_len as usize).writer(); + std::io::copy(&mut zone_file, &mut buf) + .map_err(|e| format!("copy to {} failed: {e}", path.display()))?; + let mut reader = buf.into_inner(); + + reader.set_origin(iss.origin.clone()); + + // Assume the zone is mostly sorted. Collect records for a + // name/RRtype and store a complete RRset in a hash table. + let mut records = Vec::, ZoneRecordData>>>::new(); + + for entry in reader { + let entry = entry.map_err(|err| format!("Invalid zone file: {err}"))?; + match entry { + Entry::Record(record) => { + let record: StoredRecord = record.flatten_into(); + + match record.data() { + ZoneRecordData::Rrsig(_) => (), // Ignore. + ZoneRecordData::Nsec(_) => (), // Ignore. + ZoneRecordData::Nsec3(_) => todo!(), + _ => { + if records.is_empty() { + records.push(record); + continue; + } + if record.owner() == records[0].owner() && + record.rtype() == records[0].rtype() { + records.push(record); + continue; + } + let key = (records[0].owner().clone(), records[0].rtype()); + if let Some(v) = iss.new_data.get_mut(&key) { + v.append(&mut records); + } else + { + iss.new_data.insert(key, records); + } + records = vec![]; + records.push(record); + } + } + } + Entry::Include { .. } => { + return Err(Error::from( + "Invalid zone file: $INCLUDE directive is not supported", + )); + } + } + } + + if !records.is_empty() { + let key = (records[0].owner().clone(), records[0].rtype()); + if let Some(v) = iss.new_data.get_mut(&key) { + v.append(&mut records); + } else + { + iss.new_data.insert(key, records); + } + } + Ok(()) +} + +fn initial_diffs( iss: &mut IncrementalSigningState,) { + for (_, new_rrset) in iss.new_data.iter_mut() { +println!("new rrset: {}/{}", new_rrset[0].owner(), new_rrset[0].rtype()); +//dbg!(&new_rrset); + let key = (new_rrset[0].owner().clone(), new_rrset[0].rtype()); + if let Some(mut old_rrset) = iss.old_data.remove(&key) { +//dbg!(&old_rrset); + old_rrset.sort_by(|a, b| { + a.as_ref().data().canonical_cmp(b.as_ref().data()) + }); + new_rrset.sort_by(|a, b| { + a.as_ref().data().canonical_cmp(b.as_ref().data()) + }); + + if *old_rrset != *new_rrset { + if iss.rrsigs.remove(&key).is_some() { + println!("modified, should resign {}/{}", + new_rrset[0].owner(), new_rrset[0].rtype()); + } + + } else { + } + } + else { + if let Some((added, _)) = iss.changes.get_mut(&key.0) { + added.insert(new_rrset[0].rtype()); + } else { + let mut added = HashSet::new(); + let removed = HashSet::new(); + added.insert(new_rrset[0].rtype()); + iss.changes.insert(key.0, (added, removed)); + } + } + } + for (_, old_rrset) in &iss.old_data { + // What is left in old_data is removed. + let rtype = old_rrset[0].rtype(); + let key = (old_rrset[0].owner().clone(), rtype); + + iss.rrsigs.remove(&key); + + if let Some((_, removed)) = iss.changes.get_mut(&key.0) { + removed.insert(rtype); + } else { + let added = HashSet::new(); + let mut removed = HashSet::new(); + removed.insert(rtype); + iss.changes.insert(key.0, (added, removed)); + } + } +} + +fn incremental_nsec( iss: &mut IncrementalSigningState,) { + // Should changes be sorted or not? If changes is sorted we will + // process a new delegation before any glue. Which is more efficient. + // Otherwise if glue comes first, the glue will be signed and inserted + // in the NSEC chain only to be removed when the delegation is processed. + // However, we removing a delegation, the situation is reversed. For now + // assuming that sorting is not necessary. + let changes = iss.changes.clone(); + for (key, (add, delete)) in &changes { + dbg!(key); + dbg!(add); + dbg!(delete); + if let Some(nsec) = iss.nsecs.get(key) { + println!("should handle existing name, nsec {nsec:?}"); + } else { + if add.is_empty() { + assert!(!delete.is_empty()); + // No need to do anything. + continue; + } + assert!(delete.is_empty()); + if is_occluded(key, iss) { + // No need to do anything. + continue; + } + + if add.contains(&Rtype::NS) { + nsec_set_occluded(key); + + // Create a new NSEC record and sign only DS records (if any). + let mut rtypebitmap = RtypeBitmap::::builder(); + rtypebitmap.add(Rtype::NSEC).expect("should not fail"); + rtypebitmap.add(Rtype::RRSIG).expect("should not fail"); + for rtype in add { + rtypebitmap.add(*rtype).expect("should not fail"); + } + let rtypebitmap = rtypebitmap.finalize(); + nsec_insert(key, rtypebitmap, iss); + if add.contains(&Rtype::DS) { + let mut ds_set = HashSet::new(); + ds_set.insert(Rtype::DS); + sign_rtype_set(key, &ds_set, iss); + } + continue; + } + // Create a new NSEC record and sign all records. + let mut rtypebitmap = RtypeBitmap::::builder(); + rtypebitmap.add(Rtype::NSEC).expect("should not fail"); + rtypebitmap.add(Rtype::RRSIG).expect("should not fail"); + for rtype in add { + rtypebitmap.add(*rtype).expect("should not fail"); + } + let rtypebitmap = rtypebitmap.finalize(); + nsec_insert(key, rtypebitmap, iss); + sign_rtype_set(key, add, iss); + } + } +} + +fn nsec_insert(name: &Name, rtypebitmap: RtypeBitmap, iss: &mut IncrementalSigningState) { + dbg!(&rtypebitmap); + dbg!(name); + + // Try to find the NSEC record that comes before the one we are trying + // to insert. Assume that the APEX NSEC will always exist can sort + // before anything else. + let mut range = iss.nsecs.range::, _>(..name); + let (previous_name, previous_record) = range.next_back().expect("previous NSEC record should exist"); + let previous_name = previous_name.clone(); + let previous_record = previous_record.clone(); + drop(range); + dbg!(&previous_name); + dbg!(&previous_record); + let ZoneRecordData::Nsec(previous_nsec) = previous_record.data() else { + panic!("NSEC record expected"); + }; + dbg!(previous_nsec); + let next = previous_nsec.next_name(); + let new_nsec = Nsec::new(next.clone(), rtypebitmap); + let new_record = Record::new(name.clone(), previous_record.class(), + previous_record.ttl(), ZoneRecordData::Nsec(new_nsec)); + dbg!(&new_record); + iss.nsecs.insert(name.clone(), new_record); + iss.modified_nsecs.insert(name.clone()); + let previous_nsec = Nsec::new(name.clone(), previous_nsec.types().clone()); + let previous_record = Record::new(previous_name.clone(), previous_record.class(), + previous_record.ttl(), ZoneRecordData::Nsec(previous_nsec)); + iss.nsecs.insert(previous_name.clone(), previous_record); + iss.modified_nsecs.insert(previous_name.clone()); +} + +fn nsec_set_occluded(name: &Name) { + println!("Should implement nsec_set_occluded"); +} + +fn is_occluded(name: &Name, iss: &IncrementalSigningState) -> bool { + // We need to check if the parent of name is a delegation. Stop + // when we reached origin. + let Some(mut curr) = name.parent() else { + // We asked for the parent of the root. That is weird. Just + // return no occluded. + return false; + }; + loop { + if curr == iss.origin { + // We reached apex. The name was not occluded. + return false; + } + if !curr.ends_with(&iss.origin) { + // Something weird is going on. Return not occluded. + return false; + } + if iss.new_data.get(&(curr.clone(), Rtype::NS)).is_some() { + // Name is occluded. + return true; + } + let Some(parent) = curr.parent() else { + // We asked for the parent of the root. That is weird. Just + // return no occluded. + return false; + }; + curr = parent; + } +} + +fn sign_rtype_set(name: &Name, set: &HashSet, iss: &mut IncrementalSigningState) { + for rtype in set { + let key = (name.clone(), rtype.clone()); + let Some(records) = iss.new_data.get(&key) else { + panic!("Expected something for {}/{}", name, rtype); + }; + println!("Should sign {records:?}"); + } +} + //------------ SigningMode --------------------------------------------------- #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] From c6ddd48e818b6be1c027460bbedc2eccd3b081a9 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 14 Jan 2026 11:01:06 +0100 Subject: [PATCH 02/20] Passes first test. --- src/commands/keyset/cmd.rs | 6 +- src/commands/signer.rs | 402 ++++++++++++++++++++++++++++++++++--- 2 files changed, 377 insertions(+), 31 deletions(-) diff --git a/src/commands/keyset/cmd.rs b/src/commands/keyset/cmd.rs index 0d77363e..46d7e4dc 100644 --- a/src/commands/keyset/cmd.rs +++ b/src/commands/keyset/cmd.rs @@ -166,7 +166,7 @@ type OptDuration = Option; /// Type for an optional UnixTime. A separate type is needed because CLAP /// treats Option special. -type OptUnixTime = Option; +pub type OptUnixTime = Option; /// The subcommands of the keyset utility. #[allow(clippy::large_enum_variant)] @@ -416,7 +416,7 @@ enum SetCommands { /// Set the amount inception times of signatures over the DNSKEY RRset /// are backdated. /// - /// Note that positive values are subtract from the current time. + /// Note that positive values are subtracted from the current time. DnskeyInceptionOffset { /// The offset. #[arg(value_parser = parse_duration)] @@ -4235,7 +4235,7 @@ fn parse_unixtime(value: &str) -> Result { /// Parse an optional UnixTime from a string but also allow 'off' to signal /// no UnixTime. -fn parse_opt_unixtime(value: &str) -> Result, Error> { +pub fn parse_opt_unixtime(value: &str) -> Result, Error> { if value == "off" { return Ok(None); } diff --git a/src/commands/signer.rs b/src/commands/signer.rs index 84ab05d8..1c188b49 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -10,6 +10,7 @@ use core::clone::Clone; use core::cmp::Ordering; use core::fmt::Write; use core::str::FromStr; +use domain::dnssec::sign::signatures::rrsigs::sign_rrset; use domain::rdata::dnssec::RtypeBitmap; use domain::rdata::Nsec; use domain::dep::octseq::OctetsFrom; @@ -144,6 +145,21 @@ enum Commands { #[derive(Clone, Debug, Subcommand)] enum SetCommands { + /// Set the amount inception times of signatures are backdated. + /// + /// Note that positive values are subtracted from the current time. + InceptionOffset { + /// The offset. + #[arg(value_parser = super::keyset::cmd::parse_duration)] + duration: Duration, + }, + /// Set how much time the expiration times of signatures are in the + /// future. + Lifetime { + /// The lifetime. + #[arg(value_parser = super::keyset::cmd::parse_duration)] + duration: Duration, + }, UseNsec3 { #[arg(action = clap::ArgAction::Set)] boolean: bool, @@ -185,6 +201,14 @@ enum SetCommands { NotifyCommand { args: Vec, }, + + /// Set the fake time to use when signing and other time related + /// operations. + FakeTime { + /// The time value as Unix seconds. + #[arg(value_parser = super::keyset::cmd::parse_opt_unixtime)] + opt_unixtime: super::keyset::cmd::OptUnixTime, + }, } #[derive(Default)] @@ -348,6 +372,7 @@ impl Signer { zonemd: HashSet::new(), serial_policy: SerialPolicy::Keep, notify_command: Vec::new(), + faketime: None, }; let json = serde_json::to_string_pretty(&sc).expect("should not fail"); let mut file = File::create(&self.signer_config).map_err(|e| { @@ -442,7 +467,7 @@ impl Signer { res = self.go_further(env, &sc, &mut signer_state, &kss, options) } Commands::Resign => { - self.resign(&sc, &kss, env) + self.resign(&sc, &kss, env)? } Commands::Show => { todo!(); @@ -857,9 +882,10 @@ impl Signer { nsec3_hashes = Some(hash_provider); } - let now = TestableTimestamp::now(); - let inception = (now.into_int() - sc.inception_offset.as_secs() as u32).into(); - let expiration = (now.into_int() + sc.signature_lifetime.as_secs() as u32).into(); + let now = Into::::into(sc.faketime.clone().unwrap_or(UnixTime::now())) + .as_secs() as u32; + let inception = (now - sc.inception_offset.as_secs() as u32).into(); + let expiration = (now + sc.signature_lifetime.as_secs() as u32).into(); let signing_config: SigningConfig<_, _> = match signing_mode { SigningMode::HashOnly | SigningMode::HashAndSign => { @@ -1147,10 +1173,62 @@ impl Signer { Ok(()) } - fn resign(&self, sc: &SignerConfig, kss: &KeySetState, _env: impl Env) { + fn resign(&self, sc: &SignerConfig, kss: &KeySetState, _env: impl Env) -> Result<(), Error> { let origin = kss.keyset.name(); let origin_bytes = Name::::octets_from(origin.clone()); - let mut iss = IncrementalSigningState::new(origin_bytes); + let mut iss = IncrementalSigningState::new(origin_bytes, sc); + + let mut keys = Vec::new(); + for (k, v) in kss.keyset.keys() { + let signer = match v.keytype() { + KeyType::Ksk(_) => false, + KeyType::Zsk(key_state) => key_state.signer(), + KeyType::Csk(_, key_state) => key_state.signer(), + KeyType::Include(_) => false, + }; + + if signer { + let privref = v.privref().ok_or("missing private key")?; + let priv_url = Url::parse(privref).expect("valid URL expected"); + let private_data = if priv_url.scheme() == "file" { + std::fs::read_to_string(priv_url.path()).map_err::(|e| { + format!("unable read from file {}: {e}", priv_url.path()).into() + })? + } else { + panic!("unsupported URL scheme in {priv_url}"); + }; + let secret_key = SecretKeyBytes::parse_from_bind(&private_data) + .map_err::(|e| { + format!("unable to parse private key file {privref}: {e}").into() + })?; + let pub_url = Url::parse(k).expect("valid URL expected"); + let public_data = if pub_url.scheme() == "file" { + std::fs::read_to_string(pub_url.path()).map_err::(|e| { + format!("unable read from file {}: {e}", pub_url.path()).into() + })? + } else { + panic!("unsupported URL scheme in {pub_url}"); + }; + let public_key = + parse_from_bind::(&public_data).map_err::(|e| { + format!("unable to parse public key file {k}: {e}").into() + })?; + + let key_pair = KeyPair::from_bytes(&secret_key, public_key.data()) + .map_err::(|e| { + format!("private key {privref} and public key {k} do not match: {e}").into() + })?; + let signing_key = SigningKey::new( + public_key.owner().clone(), + public_key.data().flags(), + key_pair, + ); + keys.push(signing_key); + } + } + + iss.keys = keys; + load_signed_zone(&mut iss, &sc.zonefile_out).unwrap(); // Should check if the NSEC(3) mode has changed. Sign the full @@ -1158,11 +1236,27 @@ impl Signer { load_unsigned_zone(&mut iss, &sc.zonefile_in).unwrap(); - initial_diffs(&mut iss); + load_apex_records(kss, &mut iss)?; + + initial_diffs(&mut iss)?; // Should check whether to do NSEC, NSEC3, or NSEC3 opt-out. Just do // NSEC for now. - incremental_nsec(&mut iss); + incremental_nsec(&mut iss)?; + + let mut new_sigs = vec![]; + for m in &iss.modified_nsecs { + let Some(nsec) = iss.nsecs.get(m) else { + panic!("NSEC for {m} should exist"); + }; + + let nsec = nsec.clone(); + sign_records(&[nsec], &iss.keys, iss.inception, iss.expiration, &mut new_sigs)?; + } + for (sig, rtype) in new_sigs { + let key = (sig[0].owner().clone(), rtype); + iss.rrsigs.insert(key, sig); + } let mut writer = stdout(); for (_, data) in iss.new_data { @@ -1544,6 +1638,12 @@ fn set_command( env: &impl Env, ) -> Result<(), Error> { match cmd { + SetCommands::InceptionOffset { duration } => { + sc.inception_offset = duration; + } + SetCommands::Lifetime { duration } => { + sc.signature_lifetime = duration; + } SetCommands::UseNsec3 { boolean } => { sc.use_nsec3 = boolean; } @@ -1573,6 +1673,7 @@ fn set_command( SetCommands::NotifyCommand { args } => { sc.notify_command = args; } + SetCommands::FakeTime { opt_unixtime } => sc.faketime = opt_unixtime, } *config_changed = true; Ok(()) @@ -1605,6 +1706,11 @@ struct SignerConfig { zonemd: HashSet, serial_policy: SerialPolicy, notify_command: Vec, + + /// Fake time to use when signing. + /// + /// This is need for integration tests. + faketime: Option, } #[derive(Deserialize, Serialize)] @@ -1628,10 +1734,18 @@ struct IncrementalSigningState { changes: HashMap, ChangesValue>, modified_nsecs: HashSet>, + keys: Vec>, + inception: Timestamp, + expiration: Timestamp, } impl IncrementalSigningState { - fn new(origin: Name) -> Self { + fn new(origin: Name, sc: &SignerConfig) -> Self { + let now = Into::::into(sc.faketime.clone().unwrap_or(UnixTime::now())) + .as_secs() as u32; + let inception = (now - sc.inception_offset.as_secs() as u32).into(); + let expiration = (now + sc.signature_lifetime.as_secs() as u32).into(); + Self { origin, old_data: HashMap::new(), @@ -1640,6 +1754,9 @@ impl IncrementalSigningState { rrsigs: HashMap::new(), changes: HashMap::new(), modified_nsecs: HashSet::new(), + keys: vec![], + inception, + expiration, } } } @@ -1851,13 +1968,88 @@ fn load_unsigned_zone(iss: &mut IncrementalSigningState, path: &PathBuf, ) -> Re Ok(()) } -fn initial_diffs( iss: &mut IncrementalSigningState,) { +fn load_apex_records(kss: &KeySetState, iss: &mut IncrementalSigningState) -> Result<(), Error> { + let mut records = vec![]; + let mut rrsig_records = vec![]; + for r in &kss.dnskey_rrset { + let zonefile = + domain::zonefile::inplace::Zonefile::from((r.to_string() + "\n").as_ref() as &str); + for entry in zonefile { + let entry = entry.map_err::(|e| format!("bad entry: {e}\n").into())?; + + // We only care about records in a zonefile + let Entry::Record(record) = entry else { + continue; + }; + + let owner = record.owner().to_name::(); + let data = record.data().clone().try_flatten_into().unwrap(); + let r = Record::new(owner, record.class(), record.ttl(), data); + + if r.rtype() == Rtype::RRSIG { + rrsig_records.push(r); + } else { + records.push(r); + } + } + } + + if !records.is_empty() { + let key = (records[0].owner().clone(), Rtype::DNSKEY); + iss.new_data.insert(key, records); + } + if !rrsig_records.is_empty() { + let key = (rrsig_records[0].owner().clone(), Rtype::DNSKEY); + iss.rrsigs.insert(key, rrsig_records); + } + + for r in &kss.cds_rrset { + let zonefile = + domain::zonefile::inplace::Zonefile::from((r.to_string() + "\n").as_ref() as &str); + for entry in zonefile { + let entry = entry.map_err::(|e| format!("bad entry: {e}\n").into())?; + + // We only care about records in a zonefile + let Entry::Record(record) = entry else { + continue; + }; + + let owner = record.owner().to_name::(); + let data = record.data().clone().try_flatten_into().unwrap(); + let r = Record::new(owner.clone(), record.class(), record.ttl(), data); + + if r.rtype() == Rtype::RRSIG { + let ZoneRecordData::Rrsig(rrsig) = r.data() else { + panic!("RRSIG expected"); + }; + let key = (owner, rrsig.type_covered()); + records= vec![r]; + if let Some(v) = iss.rrsigs.get_mut(&key) { + v.append(&mut records); + } else + { + iss.rrsigs.insert(key, records); + } + } else { + let key = (owner, r.rtype()); + records= vec![r]; + if let Some(v) = iss.new_data.get_mut(&key) { + v.append(&mut records); + } else + { + iss.new_data.insert(key, records); + } + } + } + } + Ok(()) +} + +fn initial_diffs( iss: &mut IncrementalSigningState,) -> Result<(), Error> { + let mut new_sigs = vec![]; for (_, new_rrset) in iss.new_data.iter_mut() { -println!("new rrset: {}/{}", new_rrset[0].owner(), new_rrset[0].rtype()); -//dbg!(&new_rrset); let key = (new_rrset[0].owner().clone(), new_rrset[0].rtype()); if let Some(mut old_rrset) = iss.old_data.remove(&key) { -//dbg!(&old_rrset); old_rrset.sort_by(|a, b| { a.as_ref().data().canonical_cmp(b.as_ref().data()) }); @@ -1867,12 +2059,9 @@ println!("new rrset: {}/{}", new_rrset[0].owner(), new_rrset[0].rtype()); if *old_rrset != *new_rrset { if iss.rrsigs.remove(&key).is_some() { - println!("modified, should resign {}/{}", - new_rrset[0].owner(), new_rrset[0].rtype()); + sign_records(new_rrset, &iss.keys, iss.inception, iss.expiration, &mut new_sigs)?; } - - } else { - } + } } else { if let Some((added, _)) = iss.changes.get_mut(&key.0) { @@ -1885,6 +2074,10 @@ println!("new rrset: {}/{}", new_rrset[0].owner(), new_rrset[0].rtype()); } } } + for (sig, rtype) in new_sigs { + let key = (sig[0].owner().clone(), rtype); + iss.rrsigs.insert(key, sig); + } for (_, old_rrset) in &iss.old_data { // What is left in old_data is removed. let rtype = old_rrset[0].rtype(); @@ -1901,22 +2094,84 @@ println!("new rrset: {}/{}", new_rrset[0].owner(), new_rrset[0].rtype()); iss.changes.insert(key.0, (added, removed)); } } + Ok(()) } -fn incremental_nsec( iss: &mut IncrementalSigningState,) { +fn incremental_nsec( iss: &mut IncrementalSigningState,) -> Result<(), Error> { // Should changes be sorted or not? If changes is sorted we will // process a new delegation before any glue. Which is more efficient. // Otherwise if glue comes first, the glue will be signed and inserted // in the NSEC chain only to be removed when the delegation is processed. // However, we removing a delegation, the situation is reversed. For now // assuming that sorting is not necessary. + + let set_nsec_rrsig: HashSet<_> = [Rtype::NSEC, Rtype::RRSIG].into(); + let changes = iss.changes.clone(); for (key, (add, delete)) in &changes { dbg!(key); dbg!(add); dbg!(delete); - if let Some(nsec) = iss.nsecs.get(key) { - println!("should handle existing name, nsec {nsec:?}"); + + // The intersection between add and delete is empty. + assert!(add.intersection(delete).next().is_none()); + + if let Some(record_nsec) = iss.nsecs.get(key) { + let record_nsec = record_nsec.clone(); + let ZoneRecordData::Nsec(nsec) = record_nsec.data() else { + panic!("NSEC record expected"); + }; + + // Convert the existing RRtype bitmap into a hash set. + let mut curr = HashSet::new(); + for rtype in nsec.types() { + curr.insert(rtype); + } + + // The intersection between curr and add is empty. + assert!(curr.intersection(add).next().is_none()); + + // delete is completely contained in curr. In other words the + // difference between delete and curr is empty. + assert!(delete.difference(&curr).next().is_none()); + + if add.contains(&Rtype::NS) { + println!("should handle existing name, adding NS nsec {nsec:?}"); + continue; + } + if delete.contains(&Rtype::NS) { + // Apex is special, but we can assume the NS RRset will not + // be removed from apex. + assert!(*key != iss.origin); + + let mut new = nsec_update_bitmap(&record_nsec, &nsec, &curr, add, delete, &set_nsec_rrsig, iss); + + // Sign the types at this name except for NSEC, and RRSIG. + new.remove(&Rtype::NSEC); + new.remove(&Rtype::RRSIG); + sign_rtype_set(key, &new, iss)?; + + // Name that were previously occluded are no longer. + nsec_clear_occluded(key); + continue; + } + if *key != iss.origin && nsec.types().contains(Rtype::NS) { + // NS marks a delegation but only when the NS is not + // at the apex. + + // If the add set contains DS then sign the DS RRset. + if add.contains(&Rtype::DS) { + let ds_set: HashSet<_> = [Rtype::DS].into(); + sign_rtype_set(key, &ds_set, iss)?; + } + nsec_update_bitmap(&record_nsec, &nsec, &curr, add, delete, &set_nsec_rrsig, iss); + continue; + } + + // The add types need to be signed. + sign_rtype_set(key, add, iss)?; + + nsec_update_bitmap(&record_nsec, &nsec, &curr, add, delete, &set_nsec_rrsig, iss); } else { if add.is_empty() { assert!(!delete.is_empty()); @@ -1942,9 +2197,8 @@ fn incremental_nsec( iss: &mut IncrementalSigningState,) { let rtypebitmap = rtypebitmap.finalize(); nsec_insert(key, rtypebitmap, iss); if add.contains(&Rtype::DS) { - let mut ds_set = HashSet::new(); - ds_set.insert(Rtype::DS); - sign_rtype_set(key, &ds_set, iss); + let ds_set: HashSet<_> = [Rtype::DS].into(); + sign_rtype_set(key, &ds_set, iss)?; } continue; } @@ -1957,9 +2211,10 @@ fn incremental_nsec( iss: &mut IncrementalSigningState,) { } let rtypebitmap = rtypebitmap.finalize(); nsec_insert(key, rtypebitmap, iss); - sign_rtype_set(key, add, iss); + sign_rtype_set(key, add, iss)?; } } + Ok(()) } fn nsec_insert(name: &Name, rtypebitmap: RtypeBitmap, iss: &mut IncrementalSigningState) { @@ -1994,10 +2249,68 @@ fn nsec_insert(name: &Name, rtypebitmap: RtypeBitmap, iss: &mut In iss.modified_nsecs.insert(previous_name.clone()); } -fn nsec_set_occluded(name: &Name) { +fn nsec_remove(name: &Name, next_name: &Name, iss: &mut IncrementalSigningState) { + dbg!(name); + dbg!(next_name); + + // Try to find the NSEC record that comes before the one we are trying + // to remove. Assume that the APEX NSEC will always exist can sort + // before anything else. + let mut range = iss.nsecs.range::, _>(..name); + let (previous_name, previous_record) = range.next_back().expect("previous NSEC record should exist"); + let previous_name = previous_name.clone(); + let previous_record = previous_record.clone(); + drop(range); + dbg!(&previous_name); + dbg!(&previous_record); + let ZoneRecordData::Nsec(previous_nsec) = previous_record.data() else { + panic!("NSEC record expected"); + }; + dbg!(previous_nsec); + let previous_nsec = Nsec::new(next_name.clone(), previous_nsec.types().clone()); + let previous_record = Record::new(previous_name.clone(), previous_record.class(), + previous_record.ttl(), ZoneRecordData::Nsec(previous_nsec)); + iss.nsecs.insert(previous_name.clone(), previous_record); + iss.modified_nsecs.insert(previous_name.clone()); + iss.nsecs.remove(name); + iss.modified_nsecs.remove(name); + let key = (name.clone(), Rtype::NSEC); + iss.rrsigs.remove(&key); +} + +// Return the effective result HashSet even when the NSEC record gets deleted. +fn nsec_update_bitmap(record: &ZRD, nsec: &Nsec>, curr: &HashSet, add: &HashSet, delete: &HashSet, set_nsec_rrsig: &HashSet, iss: &mut IncrementalSigningState) -> HashSet { + // Update curr. + let curr: HashSet<_> = curr.union(add).map(|t| *t).collect(); + let curr: HashSet<_> = curr.difference(delete).map(|t| *t).collect(); + + let owner = record.owner(); + if curr == *set_nsec_rrsig { + nsec_remove(owner, nsec.next_name(), iss); + return curr; + } + + let mut builder = RtypeBitmap::::builder(); + for rtype in &curr { + builder.add(*rtype).expect("should not fail"); + } + let nsec = Nsec::new(nsec.next_name().clone(), builder.finalize()); + let record = Record::new(record.owner().clone(), + record.class(), record.ttl(), ZoneRecordData::Nsec(nsec)); + iss.nsecs.insert(owner.clone(), record); + + iss.modified_nsecs.insert(owner.clone()); + curr +} + +fn nsec_set_occluded(_name: &Name) { println!("Should implement nsec_set_occluded"); } +fn nsec_clear_occluded(_name: &Name) { + println!("Should implement nsec_clear_occluded"); +} + fn is_occluded(name: &Name, iss: &IncrementalSigningState) -> bool { // We need to check if the parent of name is a delegation. Stop // when we reached origin. @@ -2028,16 +2341,49 @@ fn is_occluded(name: &Name, iss: &IncrementalSigningState) -> bool { } } -fn sign_rtype_set(name: &Name, set: &HashSet, iss: &mut IncrementalSigningState) { +fn sign_rtype_set(name: &Name, set: &HashSet, iss: &mut IncrementalSigningState) -> Result<(), Error> { + let mut new_sigs = vec![]; for rtype in set { let key = (name.clone(), rtype.clone()); let Some(records) = iss.new_data.get(&key) else { panic!("Expected something for {}/{}", name, rtype); }; - println!("Should sign {records:?}"); + sign_records(records, &iss.keys, iss.inception, iss.expiration, &mut new_sigs)?; } + for (sig, rtype) in new_sigs { + let key = (sig[0].owner().clone(), rtype); + iss.rrsigs.insert(key, sig); + } + Ok(()) } +fn sign_records(records: &[ZRD], keys: &[SigningKey], inception: Timestamp, expiration: Timestamp, new_sigs: &mut Vec<(Vec, Rtype)>) -> Result<(), Error> { + + let rtype = records[0].rtype(); + if rtype == Rtype::DNSKEY || rtype == Rtype::CDS || + rtype == Rtype::CDNSKEY { + // These records get signed with the KSK(s). Don't touch + // the signatures. + return Ok(()); + } + + let rrset = Rrset::new(records) + .map_err(|e| format!("Rrset::new failed: {e}"))?; + let mut rrsig_records = vec![]; + for key in keys { + let rrsig = sign_rrset(key, &rrset, + inception, + expiration) + .map_err(|e| format!("signing failed: {e}"))?; + let record = Record::new(rrsig.owner().clone(), + rrsig.class(), rrsig.ttl(), ZoneRecordData::Rrsig(rrsig.data().clone())); + rrsig_records.push(record); + } + new_sigs.push((rrsig_records, rrset.rtype())); + Ok(()) +} + + //------------ SigningMode --------------------------------------------------- #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] From bf8106f0ea41d9e8699f7e80d590f4387528937a Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 14 Jan 2026 15:48:09 +0100 Subject: [PATCH 03/20] Changes to pass test2/NSEC. --- src/commands/signer.rs | 47 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/commands/signer.rs b/src/commands/signer.rs index 1c188b49..69387dab 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -2185,8 +2185,6 @@ fn incremental_nsec( iss: &mut IncrementalSigningState,) -> Result<(), Error> { } if add.contains(&Rtype::NS) { - nsec_set_occluded(key); - // Create a new NSEC record and sign only DS records (if any). let mut rtypebitmap = RtypeBitmap::::builder(); rtypebitmap.add(Rtype::NSEC).expect("should not fail"); @@ -2200,6 +2198,10 @@ fn incremental_nsec( iss: &mut IncrementalSigningState,) -> Result<(), Error> { let ds_set: HashSet<_> = [Rtype::DS].into(); sign_rtype_set(key, &ds_set, iss)?; } + + // nsec_set_occluded expects the NSEC for key to exist. + // So call this after inserting the new NSEC record. + nsec_set_occluded(key, iss); continue; } // Create a new NSEC record and sign all records. @@ -2303,8 +2305,45 @@ fn nsec_update_bitmap(record: &ZRD, nsec: &Nsec>, curr: &Hash curr } -fn nsec_set_occluded(_name: &Name) { - println!("Should implement nsec_set_occluded"); +fn nsec_set_occluded(name: &Name, iss: &mut IncrementalSigningState) { + let Some(nsec_record) = iss.nsecs.get(name) else { + panic!("NSEC for {name} expected to exist"); + }; + let ZoneRecordData::Nsec(nsec) = nsec_record.data() else { + panic!("NSEC record expected"); + }; + let nsec = nsec.clone(); + let mut next = nsec.next_name().clone(); + loop { + if !next.ends_with(name) { + break; + } + + // For consistency, make sure next is not equal to name. + if next == name { + break; + } + dbg!(&next); + + let curr = next; + let Some(nsec_record) = iss.nsecs.get(&curr) else { + panic!("NSEC for {name} expected to exist"); + }; + dbg!(nsec_record); + let ZoneRecordData::Nsec(nsec) = nsec_record.data() else { + panic!("NSEC record expected"); + }; + let nsec = nsec.clone(); + next = nsec.next_name().clone(); + + nsec_remove(&curr, &next, iss); + + // Remove all signatures. + for rtype in nsec.types().iter() { + let key = (curr.clone(), rtype); + iss.rrsigs.remove(&key); + } + } } fn nsec_clear_occluded(_name: &Name) { From 913da3146ebc35c5d580d8d349a6bfb34d508ca2 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Thu, 15 Jan 2026 11:51:59 +0100 Subject: [PATCH 04/20] Now passes test3/NSEC. --- src/commands/signer.rs | 143 ++++++++++++++++++++++++++++++++++------- 1 file changed, 118 insertions(+), 25 deletions(-) diff --git a/src/commands/signer.rs b/src/commands/signer.rs index 69387dab..a7c4724e 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -1728,7 +1728,7 @@ type ChangesValue = (RtypeSet, RtypeSet); // add set followed by delete set. struct IncrementalSigningState { origin: Name, old_data: HashMap<(Name, Rtype), Vec>, - new_data: HashMap<(Name, Rtype), Vec>, + new_data: BTreeMap<(Name, Rtype), Vec>, nsecs: BTreeMap, ZRD>, rrsigs: HashMap<(Name, Rtype), Vec>, @@ -1749,7 +1749,7 @@ impl IncrementalSigningState { Self { origin, old_data: HashMap::new(), - new_data: HashMap::new(), + new_data: BTreeMap::new(), nsecs: BTreeMap::new(), rrsigs: HashMap::new(), changes: HashMap::new(), @@ -2109,9 +2109,6 @@ fn incremental_nsec( iss: &mut IncrementalSigningState,) -> Result<(), Error> { let changes = iss.changes.clone(); for (key, (add, delete)) in &changes { - dbg!(key); - dbg!(add); - dbg!(delete); // The intersection between add and delete is empty. assert!(add.intersection(delete).next().is_none()); @@ -2136,7 +2133,34 @@ fn incremental_nsec( iss: &mut IncrementalSigningState,) -> Result<(), Error> { assert!(delete.difference(&curr).next().is_none()); if add.contains(&Rtype::NS) { - println!("should handle existing name, adding NS nsec {nsec:?}"); + // Remove the signatures for the existing types. + for rtype in nsec.types().iter() { + // When NS is added, we should keep the signatures for + // DS and NSEC. The NSEC signature will be updated but + // there is no point in removing it first. Do no try to + // remove a signature for RRSIG because it does not exist. + if rtype == Rtype::DS || rtype == Rtype::NSEC || + rtype == Rtype::RRSIG { + continue; + } + let key = (key.clone(), rtype); + iss.rrsigs.remove(&key); + } + + // Restrict curr and add to these types. + let mask: HashSet = [Rtype::NS, Rtype::DS, Rtype::NSEC, Rtype::RRSIG].into(); + + let curr: HashSet = curr.intersection(&mask).map(|r| *r).collect(); + let add: HashSet = add.intersection(&mask).map(|r| *r).collect(); + + // Update the NSEC record. + nsec_update_bitmap(&record_nsec, &nsec, &curr, &add, delete, &set_nsec_rrsig, iss); + + // Mark descendents as occluded after updating the bitmap. + // The reason is that nsec_update_bitmap uses that current + // next_name and nsec_set_occluded may change that. + nsec_set_occluded(key, iss); + continue; } if delete.contains(&Rtype::NS) { @@ -2144,6 +2168,21 @@ fn incremental_nsec( iss: &mut IncrementalSigningState,) -> Result<(), Error> { // be removed from apex. assert!(*key != iss.origin); + // Curr does not include all types at this name. Add the + // missing types to curr. + let range_key = (key.clone(), 0.into()); + let range = iss.new_data.range(range_key..); + for ((r_name, r_type), _) in range { + if r_name != key { + break; + } + if add.contains(r_type) { + // Skip what we are trying to add. + continue; + } + curr.insert(*r_type); + } + let mut new = nsec_update_bitmap(&record_nsec, &nsec, &curr, add, delete, &set_nsec_rrsig, iss); // Sign the types at this name except for NSEC, and RRSIG. @@ -2152,7 +2191,7 @@ fn incremental_nsec( iss: &mut IncrementalSigningState,) -> Result<(), Error> { sign_rtype_set(key, &new, iss)?; // Name that were previously occluded are no longer. - nsec_clear_occluded(key); + nsec_clear_occluded(key, iss)?; continue; } if *key != iss.origin && nsec.types().contains(Rtype::NS) { @@ -2220,9 +2259,6 @@ fn incremental_nsec( iss: &mut IncrementalSigningState,) -> Result<(), Error> { } fn nsec_insert(name: &Name, rtypebitmap: RtypeBitmap, iss: &mut IncrementalSigningState) { - dbg!(&rtypebitmap); - dbg!(name); - // Try to find the NSEC record that comes before the one we are trying // to insert. Assume that the APEX NSEC will always exist can sort // before anything else. @@ -2231,17 +2267,13 @@ fn nsec_insert(name: &Name, rtypebitmap: RtypeBitmap, iss: &mut In let previous_name = previous_name.clone(); let previous_record = previous_record.clone(); drop(range); - dbg!(&previous_name); - dbg!(&previous_record); let ZoneRecordData::Nsec(previous_nsec) = previous_record.data() else { panic!("NSEC record expected"); }; - dbg!(previous_nsec); let next = previous_nsec.next_name(); let new_nsec = Nsec::new(next.clone(), rtypebitmap); let new_record = Record::new(name.clone(), previous_record.class(), previous_record.ttl(), ZoneRecordData::Nsec(new_nsec)); - dbg!(&new_record); iss.nsecs.insert(name.clone(), new_record); iss.modified_nsecs.insert(name.clone()); let previous_nsec = Nsec::new(name.clone(), previous_nsec.types().clone()); @@ -2252,9 +2284,6 @@ fn nsec_insert(name: &Name, rtypebitmap: RtypeBitmap, iss: &mut In } fn nsec_remove(name: &Name, next_name: &Name, iss: &mut IncrementalSigningState) { - dbg!(name); - dbg!(next_name); - // Try to find the NSEC record that comes before the one we are trying // to remove. Assume that the APEX NSEC will always exist can sort // before anything else. @@ -2263,12 +2292,9 @@ fn nsec_remove(name: &Name, next_name: &Name, iss: &mut Incrementa let previous_name = previous_name.clone(); let previous_record = previous_record.clone(); drop(range); - dbg!(&previous_name); - dbg!(&previous_record); let ZoneRecordData::Nsec(previous_nsec) = previous_record.data() else { panic!("NSEC record expected"); }; - dbg!(previous_nsec); let previous_nsec = Nsec::new(next_name.clone(), previous_nsec.types().clone()); let previous_record = Record::new(previous_name.clone(), previous_record.class(), previous_record.ttl(), ZoneRecordData::Nsec(previous_nsec)); @@ -2323,13 +2349,10 @@ fn nsec_set_occluded(name: &Name, iss: &mut IncrementalSigningState) { if next == name { break; } - dbg!(&next); - let curr = next; let Some(nsec_record) = iss.nsecs.get(&curr) else { panic!("NSEC for {name} expected to exist"); }; - dbg!(nsec_record); let ZoneRecordData::Nsec(nsec) = nsec_record.data() else { panic!("NSEC record expected"); }; @@ -2346,8 +2369,78 @@ fn nsec_set_occluded(name: &Name, iss: &mut IncrementalSigningState) { } } -fn nsec_clear_occluded(_name: &Name) { - println!("Should implement nsec_clear_occluded"); +fn nsec_clear_occluded(name: &Name, iss: &mut IncrementalSigningState) -> Result<(), Error> { + let key = (name.clone(), Rtype::SOA); + let range = iss.new_data.range(key..); + let mut opt_curr_name: Option<&Name> = None; + let mut curr_types: HashSet = HashSet::new(); + let mut work = vec![]; + + // Keep track of delegations. Name below a delegation remain occluded. + let mut delegation: Option> = None; + + for ((key_name, key_rtype), _) in range { + // There is no easy way to avoid name showing up in the range. Just + // filter out name. + if key_name == name { + continue; + } + + // Make sure curr_name is below name. + if !key_name.ends_with(name) { + break; + } + if let Some(d) = &delegation { + if key_name.ends_with(d) && key_name != d { + // Skip. + continue; + } + } + if *key_rtype == Rtype::NS { + // Set key_name as a delegation. + delegation = Some(key_name.clone()); + } + if let Some(curr_name) = opt_curr_name { + if key_name == curr_name { + curr_types.insert(*key_rtype); + } else { + work.push((curr_name.clone(), curr_types)); + opt_curr_name = Some(key_name); + curr_types = [*key_rtype].into(); + } + } else { + opt_curr_name = Some(key_name); + curr_types.insert(*key_rtype); + } + } + if let Some(curr_name) = opt_curr_name { + work.push((curr_name.clone(), curr_types)); + } + for (curr_name, curr_types) in work { + let mut curr_types = if curr_types.contains(&Rtype::NS) { + let has_ds = curr_types.contains(&Rtype::DS); + let mut curr_types: HashSet = [Rtype::NS].into(); + if has_ds { + curr_types.insert(Rtype::DS); + } + curr_types + } else { + curr_types + }; + let mut rtypebitmap = RtypeBitmap::::builder(); + rtypebitmap.add(Rtype::NSEC).expect("should not fail"); + rtypebitmap.add(Rtype::RRSIG).expect("should not fail"); + for rtype in &curr_types { + rtypebitmap.add(*rtype).expect("should not fail"); + } + let rtypebitmap = rtypebitmap.finalize(); + + // Make sure NS doesn't get signed. + curr_types.remove(&Rtype::NS); + sign_rtype_set(&curr_name, &curr_types, iss)?; + nsec_insert(&curr_name, rtypebitmap, iss); + } + Ok(()) } fn is_occluded(name: &Name, iss: &IncrementalSigningState) -> bool { From c0fbbf342bb1bfb1ab04a0c9799caa898d55539c Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Thu, 15 Jan 2026 14:25:00 +0100 Subject: [PATCH 05/20] A bit of cleanup. --- src/commands/signer.rs | 1369 ++++++++++++++++++++-------------------- 1 file changed, 702 insertions(+), 667 deletions(-) diff --git a/src/commands/signer.rs b/src/commands/signer.rs index a7c4724e..4e57660b 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -10,10 +10,6 @@ use core::clone::Clone; use core::cmp::Ordering; use core::fmt::Write; use core::str::FromStr; -use domain::dnssec::sign::signatures::rrsigs::sign_rrset; -use domain::rdata::dnssec::RtypeBitmap; -use domain::rdata::Nsec; -use domain::dep::octseq::OctetsFrom; use domain::base::iana::nsec3::Nsec3HashAlgorithm; use domain::base::iana::zonemd::{ZonemdAlgorithm, ZonemdScheme}; use domain::base::iana::Class; @@ -23,6 +19,7 @@ use domain::base::{ CanonicalOrd, Name, NameBuilder, Record, RecordData, Rtype, Serial, ToName, Ttl, }; use domain::crypto::sign::{KeyPair, SecretKeyBytes}; +use domain::dep::octseq::OctetsFrom; use domain::dnssec::common::parse_from_bind; use domain::dnssec::sign::denial::config::DenialConfig; use domain::dnssec::sign::denial::nsec::GenerateNsecConfig; @@ -31,12 +28,13 @@ use domain::dnssec::sign::error::SigningError; use domain::dnssec::sign::keys::keyset::{KeyType, UnixTime}; use domain::dnssec::sign::keys::SigningKey; use domain::dnssec::sign::records::{OwnerRrs, RecordsIter, Rrset, SortedRecords}; +use domain::dnssec::sign::signatures::rrsigs::sign_rrset; use domain::dnssec::sign::traits::{Signable, SignableZoneInPlace}; use domain::dnssec::sign::SigningConfig; use domain::dnssec::validator::base::DnskeyExt; -use domain::rdata::dnssec::Timestamp; +use domain::rdata::dnssec::{RtypeBitmap, Timestamp}; use domain::rdata::nsec3::Nsec3Salt; -use domain::rdata::{Dnskey, Nsec3, Nsec3param, Rrsig, Soa, ZoneRecordData, Zonemd}; +use domain::rdata::{Dnskey, Nsec, Nsec3, Nsec3param, Rrsig, Soa, ZoneRecordData, Zonemd}; use domain::utils::base64; use domain::zonefile::inplace::{self, Entry}; use domain::zonetree::types::StoredRecordData; @@ -48,14 +46,12 @@ use octseq::builder::with_infallible; use rayon::slice::ParallelSliceMut; use ring::digest; use serde::{Deserialize, Serialize}; -use std::io::stdout; -use std::collections::BTreeMap; use std::cmp::min; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt::{self, Display}; use std::fs::{metadata, File}; use std::io::Write as IoWrite; -use std::io::{self, BufWriter}; +use std::io::{self, stdout, BufWriter}; use std::path::{Path, PathBuf}; use std::process::Command; use std::time::UNIX_EPOCH; @@ -372,7 +368,7 @@ impl Signer { zonemd: HashSet::new(), serial_policy: SerialPolicy::Keep, notify_command: Vec::new(), - faketime: None, + faketime: None, }; let json = serde_json::to_string_pretty(&sc).expect("should not fail"); let mut file = File::create(&self.signer_config).map_err(|e| { @@ -466,9 +462,7 @@ impl Signer { state_changed = true; res = self.go_further(env, &sc, &mut signer_state, &kss, options) } - Commands::Resign => { - self.resign(&sc, &kss, env)? - } + Commands::Resign => self.resign(&sc, &kss, env)?, Commands::Show => { todo!(); } @@ -882,8 +876,8 @@ impl Signer { nsec3_hashes = Some(hash_provider); } - let now = Into::::into(sc.faketime.clone().unwrap_or(UnixTime::now())) - .as_secs() as u32; + let now = + Into::::into(sc.faketime.clone().unwrap_or(UnixTime::now())).as_secs() as u32; let inception = (now - sc.inception_offset.as_secs() as u32).into(); let expiration = (now + sc.signature_lifetime.as_secs() as u32).into(); @@ -1174,9 +1168,9 @@ impl Signer { } fn resign(&self, sc: &SignerConfig, kss: &KeySetState, _env: impl Env) -> Result<(), Error> { - let origin = kss.keyset.name(); - let origin_bytes = Name::::octets_from(origin.clone()); - let mut iss = IncrementalSigningState::new(origin_bytes, sc); + let origin = kss.keyset.name(); + let origin_bytes = Name::::octets_from(origin.clone()); + let mut iss = IncrementalSigningState::new(origin_bytes, sc); let mut keys = Vec::new(); for (k, v) in kss.keyset.keys() { @@ -1227,56 +1221,68 @@ impl Signer { } } - iss.keys = keys; - - load_signed_zone(&mut iss, &sc.zonefile_out).unwrap(); - - // Should check if the NSEC(3) mode has changed. Sign the full - // zone if that has happened. - - load_unsigned_zone(&mut iss, &sc.zonefile_in).unwrap(); - - load_apex_records(kss, &mut iss)?; - - initial_diffs(&mut iss)?; - - // Should check whether to do NSEC, NSEC3, or NSEC3 opt-out. Just do - // NSEC for now. - incremental_nsec(&mut iss)?; - - let mut new_sigs = vec![]; - for m in &iss.modified_nsecs { - let Some(nsec) = iss.nsecs.get(m) else { - panic!("NSEC for {m} should exist"); - }; - - let nsec = nsec.clone(); - sign_records(&[nsec], &iss.keys, iss.inception, iss.expiration, &mut new_sigs)?; - } - for (sig, rtype) in new_sigs { - let key = (sig[0].owner().clone(), rtype); - iss.rrsigs.insert(key, sig); - } - - let mut writer = stdout(); - for (_, data) in iss.new_data { - for rr in data { - writer.write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))); - } - } - for (_, rr) in iss.nsecs { - writer.write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))); - } - for (_, data) in iss.rrsigs { - for rr in data { - let ZoneRecordData::Rrsig(rrsig) = rr.data() else { - panic!("RRSIG expected"); - }; - let rr = Record::new(rr.owner(), rr.class(), rr.ttl(), YyyyMmDdHhMMSsRrsig(rrsig)); - writer.write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))); - } - } - todo!(); + iss.keys = keys; + + load_signed_zone(&mut iss, &sc.zonefile_out).unwrap(); + + // Should check if the NSEC(3) mode has changed. Sign the full + // zone if that has happened. + + load_unsigned_zone(&mut iss, &sc.zonefile_in).unwrap(); + + load_apex_records(kss, &mut iss)?; + + initial_diffs(&mut iss)?; + + // Should check whether to do NSEC, NSEC3, or NSEC3 opt-out. Just do + // NSEC for now. + incremental_nsec(&mut iss)?; + + let mut new_sigs = vec![]; + for m in &iss.modified_nsecs { + let Some(nsec) = iss.nsecs.get(m) else { + panic!("NSEC for {m} should exist"); + }; + + let nsec = nsec.clone(); + sign_records( + &[nsec], + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + } + for (sig, rtype) in new_sigs { + let key = (sig[0].owner().clone(), rtype); + iss.rrsigs.insert(key, sig); + } + + let mut writer = stdout(); + for (_, data) in iss.new_data { + for rr in data { + writer + .write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))) + .map_err(|e| format!("unable write signed zone: {e}"))?; + } + } + for (_, rr) in iss.nsecs { + writer + .write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))) + .map_err(|e| format!("unable write signed zone: {e}"))?; + } + for (_, data) in iss.rrsigs { + for rr in data { + let ZoneRecordData::Rrsig(rrsig) = rr.data() else { + panic!("RRSIG expected"); + }; + let rr = Record::new(rr.owner(), rr.class(), rr.ttl(), YyyyMmDdHhMMSsRrsig(rrsig)); + writer + .write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))) + .map_err(|e| format!("unable write signed zone: {e}"))?; + } + } + todo!(); } fn write_rr>( @@ -1638,12 +1644,12 @@ fn set_command( env: &impl Env, ) -> Result<(), Error> { match cmd { - SetCommands::InceptionOffset { duration } => { - sc.inception_offset = duration; - } - SetCommands::Lifetime { duration } => { - sc.signature_lifetime = duration; - } + SetCommands::InceptionOffset { duration } => { + sc.inception_offset = duration; + } + SetCommands::Lifetime { duration } => { + sc.signature_lifetime = duration; + } SetCommands::UseNsec3 { boolean } => { sc.use_nsec3 = boolean; } @@ -1673,7 +1679,7 @@ fn set_command( SetCommands::NotifyCommand { args } => { sc.notify_command = args; } - SetCommands::FakeTime { opt_unixtime } => sc.faketime = opt_unixtime, + SetCommands::FakeTime { opt_unixtime } => sc.faketime = opt_unixtime, } *config_changed = true; Ok(()) @@ -1741,51 +1747,43 @@ struct IncrementalSigningState { impl IncrementalSigningState { fn new(origin: Name, sc: &SignerConfig) -> Self { - let now = Into::::into(sc.faketime.clone().unwrap_or(UnixTime::now())) - .as_secs() as u32; + let now = + Into::::into(sc.faketime.clone().unwrap_or(UnixTime::now())).as_secs() as u32; let inception = (now - sc.inception_offset.as_secs() as u32).into(); let expiration = (now + sc.signature_lifetime.as_secs() as u32).into(); - Self { - origin, - old_data: HashMap::new(), - new_data: BTreeMap::new(), - nsecs: BTreeMap::new(), - rrsigs: HashMap::new(), - changes: HashMap::new(), - modified_nsecs: HashSet::new(), - keys: vec![], - inception, - expiration, - } + Self { + origin, + old_data: HashMap::new(), + new_data: BTreeMap::new(), + nsecs: BTreeMap::new(), + rrsigs: HashMap::new(), + changes: HashMap::new(), + modified_nsecs: HashSet::new(), + keys: vec![], + inception, + expiration, + } } } type ZRD = Record, ZoneRecordData>>; -fn load_signed_zone(iss: &mut IncrementalSigningState, path: &PathBuf, ) -> Result<(), Error> { +fn load_signed_zone(iss: &mut IncrementalSigningState, path: &PathBuf) -> Result<(), Error> { // Don't use Zonefile::load() as it knows nothing about the size of // the original file so uses default allocation which allocates more // bytes than are needed. Instead control the allocation size based on // our knowledge of the file size. let mut zone_file = File::open(path) - .map_err(|e| format!("open failed: {e}").into()) - .context(&format!( - "loading zone file from path '{}'", - path.display(), - ))?; + .map_err(|e| format!("open failed: {e}").into()) + .context(&format!("loading zone file from path '{}'", path.display(),))?; let zone_file_len = zone_file - .metadata() - .map_err(|e| { - format!( - "unable to get metadata from {}: {e}", - path.display() - ) - })? - .len(); + .metadata() + .map_err(|e| format!("unable to get metadata from {}: {e}", path.display()))? + .len(); let mut buf = inplace::Zonefile::with_capacity(zone_file_len as usize).writer(); std::io::copy(&mut zone_file, &mut buf) - .map_err(|e| format!("copy to {} failed: {e}", path.display()))?; + .map_err(|e| format!("copy to {} failed: {e}", path.display()))?; let mut reader = buf.into_inner(); reader.set_origin(iss.origin.clone()); @@ -1797,117 +1795,107 @@ fn load_signed_zone(iss: &mut IncrementalSigningState, path: &PathBuf, ) -> Resu let mut type_covered = Rtype::RRSIG; for entry in reader { - let entry = entry.map_err(|err| format!("Invalid zone file: {err}"))?; - match entry { - Entry::Record(record) => { - let record: StoredRecord = record.flatten_into(); - - match record.data() { - ZoneRecordData::Rrsig(rrsig) => { - if rrsig_records.is_empty() { - type_covered = rrsig.type_covered(); - rrsig_records.push(record); - continue; - } - if record.owner() == rrsig_records[0].owner() && - rrsig.type_covered() == type_covered { - todo!(); - } - - let key = (rrsig_records[0].owner().clone(), type_covered); - if let Some(v) = iss.rrsigs.get_mut(&key) { - v.append(&mut rrsig_records); - } else - { - iss.rrsigs.insert(key, rrsig_records); - } - type_covered = rrsig.type_covered(); - rrsig_records = vec![]; - rrsig_records.push(record); - } - ZoneRecordData::Nsec(_) => { - // Assume (at most) one NSEC record per owner name. - // Directly insert into the hash map. - iss.nsecs.insert(record.owner().clone(), record); - } - ZoneRecordData::Nsec3(_) => todo!(), - _ => { - if records.is_empty() { - records.push(record); - continue; - } - if record.owner() == records[0].owner() && - record.rtype() == records[0].rtype() { - records.push(record); - continue; - } - let key = (records[0].owner().clone(), records[0].rtype()); - if let Some(v) = iss.old_data.get_mut(&key) { - v.append(&mut records); - } else - { - iss.old_data.insert(key, records); - } - records = vec![]; - records.push(record); - } - } - } - Entry::Include { .. } => { - return Err(Error::from( - "Invalid zone file: $INCLUDE directive is not supported", - )); - } - } + let entry = entry.map_err(|err| format!("Invalid zone file: {err}"))?; + match entry { + Entry::Record(record) => { + let record: StoredRecord = record.flatten_into(); + + match record.data() { + ZoneRecordData::Rrsig(rrsig) => { + if rrsig_records.is_empty() { + type_covered = rrsig.type_covered(); + rrsig_records.push(record); + continue; + } + if record.owner() == rrsig_records[0].owner() + && rrsig.type_covered() == type_covered + { + todo!(); + } + + let key = (rrsig_records[0].owner().clone(), type_covered); + if let Some(v) = iss.rrsigs.get_mut(&key) { + v.append(&mut rrsig_records); + } else { + iss.rrsigs.insert(key, rrsig_records); + } + type_covered = rrsig.type_covered(); + rrsig_records = vec![]; + rrsig_records.push(record); + } + ZoneRecordData::Nsec(_) => { + // Assume (at most) one NSEC record per owner name. + // Directly insert into the hash map. + iss.nsecs.insert(record.owner().clone(), record); + } + ZoneRecordData::Nsec3(_) => todo!(), + _ => { + if records.is_empty() { + records.push(record); + continue; + } + if record.owner() == records[0].owner() + && record.rtype() == records[0].rtype() + { + records.push(record); + continue; + } + let key = (records[0].owner().clone(), records[0].rtype()); + if let Some(v) = iss.old_data.get_mut(&key) { + v.append(&mut records); + } else { + iss.old_data.insert(key, records); + } + records = vec![]; + records.push(record); + } + } + } + Entry::Include { .. } => { + return Err(Error::from( + "Invalid zone file: $INCLUDE directive is not supported", + )); + } + } } if !records.is_empty() { - let key = (records[0].owner().clone(), records[0].rtype()); - if let Some(v) = iss.old_data.get_mut(&key) { - v.append(&mut records); - } else - { - iss.old_data.insert(key, records); - } + let key = (records[0].owner().clone(), records[0].rtype()); + if let Some(v) = iss.old_data.get_mut(&key) { + v.append(&mut records); + } else { + iss.old_data.insert(key, records); + } } if !rrsig_records.is_empty() { - let key = (rrsig_records[0].owner().clone(), type_covered); - if let Some(v) = iss.rrsigs.get_mut(&key) { - v.append(&mut rrsig_records); - } else - { - iss.rrsigs.insert(key, rrsig_records); - } + let key = (rrsig_records[0].owner().clone(), type_covered); + if let Some(v) = iss.rrsigs.get_mut(&key) { + v.append(&mut rrsig_records); + } else { + iss.rrsigs.insert(key, rrsig_records); + } } Ok(()) } -fn load_unsigned_zone(iss: &mut IncrementalSigningState, path: &PathBuf, ) -> Result<(), Error> { -// Basically a copy of load_signed_zone execpt that signature and NSEC(3) -// records are removed. Make sure to delete and update APEX records. +fn load_unsigned_zone(iss: &mut IncrementalSigningState, path: &PathBuf) -> Result<(), Error> { + // Basically a copy of load_signed_zone execpt that signature and NSEC(3) + // records are removed. Make sure to delete and update APEX records. // Don't use Zonefile::load() as it knows nothing about the size of // the original file so uses default allocation which allocates more // bytes than are needed. Instead control the allocation size based on // our knowledge of the file size. let mut zone_file = File::open(path) - .map_err(|e| format!("open failed: {e}").into()) - .context(&format!( - "loading zone file from path '{}'", - path.display(), - ))?; + .map_err(|e| format!("open failed: {e}").into()) + .context(&format!("loading zone file from path '{}'", path.display(),))?; let zone_file_len = zone_file - .metadata() - .map_err(|e| { - format!( - "unable to get metadata from {}: {e}", - path.display() - ) - })? - .len(); + .metadata() + .map_err(|e| format!("unable to get metadata from {}: {e}", path.display()))? + .len(); let mut buf = inplace::Zonefile::with_capacity(zone_file_len as usize).writer(); std::io::copy(&mut zone_file, &mut buf) - .map_err(|e| format!("copy to {} failed: {e}", path.display()))?; + .map_err(|e| format!("copy to {} failed: {e}", path.display()))?; let mut reader = buf.into_inner(); reader.set_origin(iss.origin.clone()); @@ -1917,188 +1905,186 @@ fn load_unsigned_zone(iss: &mut IncrementalSigningState, path: &PathBuf, ) -> Re let mut records = Vec::, ZoneRecordData>>>::new(); for entry in reader { - let entry = entry.map_err(|err| format!("Invalid zone file: {err}"))?; - match entry { - Entry::Record(record) => { - let record: StoredRecord = record.flatten_into(); - - match record.data() { - ZoneRecordData::Rrsig(_) => (), // Ignore. - ZoneRecordData::Nsec(_) => (), // Ignore. - ZoneRecordData::Nsec3(_) => todo!(), - _ => { - if records.is_empty() { - records.push(record); - continue; - } - if record.owner() == records[0].owner() && - record.rtype() == records[0].rtype() { - records.push(record); - continue; - } - let key = (records[0].owner().clone(), records[0].rtype()); - if let Some(v) = iss.new_data.get_mut(&key) { - v.append(&mut records); - } else - { - iss.new_data.insert(key, records); - } - records = vec![]; - records.push(record); - } - } - } - Entry::Include { .. } => { - return Err(Error::from( - "Invalid zone file: $INCLUDE directive is not supported", - )); - } - } + let entry = entry.map_err(|err| format!("Invalid zone file: {err}"))?; + match entry { + Entry::Record(record) => { + let record: StoredRecord = record.flatten_into(); + + match record.data() { + ZoneRecordData::Rrsig(_) => (), // Ignore. + ZoneRecordData::Nsec(_) => (), // Ignore. + ZoneRecordData::Nsec3(_) => todo!(), + _ => { + if records.is_empty() { + records.push(record); + continue; + } + if record.owner() == records[0].owner() + && record.rtype() == records[0].rtype() + { + records.push(record); + continue; + } + let key = (records[0].owner().clone(), records[0].rtype()); + if let Some(v) = iss.new_data.get_mut(&key) { + v.append(&mut records); + } else { + iss.new_data.insert(key, records); + } + records = vec![]; + records.push(record); + } + } + } + Entry::Include { .. } => { + return Err(Error::from( + "Invalid zone file: $INCLUDE directive is not supported", + )); + } + } } if !records.is_empty() { - let key = (records[0].owner().clone(), records[0].rtype()); - if let Some(v) = iss.new_data.get_mut(&key) { - v.append(&mut records); - } else - { - iss.new_data.insert(key, records); - } + let key = (records[0].owner().clone(), records[0].rtype()); + if let Some(v) = iss.new_data.get_mut(&key) { + v.append(&mut records); + } else { + iss.new_data.insert(key, records); + } } Ok(()) } fn load_apex_records(kss: &KeySetState, iss: &mut IncrementalSigningState) -> Result<(), Error> { - let mut records = vec![]; - let mut rrsig_records = vec![]; - for r in &kss.dnskey_rrset { - let zonefile = - domain::zonefile::inplace::Zonefile::from((r.to_string() + "\n").as_ref() as &str); - for entry in zonefile { - let entry = entry.map_err::(|e| format!("bad entry: {e}\n").into())?; - - // We only care about records in a zonefile - let Entry::Record(record) = entry else { - continue; - }; + let mut records = vec![]; + let mut rrsig_records = vec![]; + for r in &kss.dnskey_rrset { + let zonefile = + domain::zonefile::inplace::Zonefile::from((r.to_string() + "\n").as_ref() as &str); + for entry in zonefile { + let entry = entry.map_err::(|e| format!("bad entry: {e}\n").into())?; + + // We only care about records in a zonefile + let Entry::Record(record) = entry else { + continue; + }; - let owner = record.owner().to_name::(); - let data = record.data().clone().try_flatten_into().unwrap(); - let r = Record::new(owner, record.class(), record.ttl(), data); + let owner = record.owner().to_name::(); + let data = record.data().clone().try_flatten_into().unwrap(); + let r = Record::new(owner, record.class(), record.ttl(), data); - if r.rtype() == Rtype::RRSIG { - rrsig_records.push(r); - } else { - records.push(r); - } + if r.rtype() == Rtype::RRSIG { + rrsig_records.push(r); + } else { + records.push(r); } } + } - if !records.is_empty() { - let key = (records[0].owner().clone(), Rtype::DNSKEY); - iss.new_data.insert(key, records); - } - if !rrsig_records.is_empty() { - let key = (rrsig_records[0].owner().clone(), Rtype::DNSKEY); - iss.rrsigs.insert(key, rrsig_records); - } + if !records.is_empty() { + let key = (records[0].owner().clone(), Rtype::DNSKEY); + iss.new_data.insert(key, records); + } + if !rrsig_records.is_empty() { + let key = (rrsig_records[0].owner().clone(), Rtype::DNSKEY); + iss.rrsigs.insert(key, rrsig_records); + } - for r in &kss.cds_rrset { - let zonefile = - domain::zonefile::inplace::Zonefile::from((r.to_string() + "\n").as_ref() as &str); - for entry in zonefile { - let entry = entry.map_err::(|e| format!("bad entry: {e}\n").into())?; + for r in &kss.cds_rrset { + let zonefile = + domain::zonefile::inplace::Zonefile::from((r.to_string() + "\n").as_ref() as &str); + for entry in zonefile { + let entry = entry.map_err::(|e| format!("bad entry: {e}\n").into())?; - // We only care about records in a zonefile - let Entry::Record(record) = entry else { - continue; - }; + // We only care about records in a zonefile + let Entry::Record(record) = entry else { + continue; + }; - let owner = record.owner().to_name::(); - let data = record.data().clone().try_flatten_into().unwrap(); - let r = Record::new(owner.clone(), record.class(), record.ttl(), data); - - if r.rtype() == Rtype::RRSIG { - let ZoneRecordData::Rrsig(rrsig) = r.data() else { - panic!("RRSIG expected"); - }; - let key = (owner, rrsig.type_covered()); - records= vec![r]; - if let Some(v) = iss.rrsigs.get_mut(&key) { - v.append(&mut records); - } else - { - iss.rrsigs.insert(key, records); - } - } else { - let key = (owner, r.rtype()); - records= vec![r]; - if let Some(v) = iss.new_data.get_mut(&key) { - v.append(&mut records); - } else - { - iss.new_data.insert(key, records); - } - } + let owner = record.owner().to_name::(); + let data = record.data().clone().try_flatten_into().unwrap(); + let r = Record::new(owner.clone(), record.class(), record.ttl(), data); + + if r.rtype() == Rtype::RRSIG { + let ZoneRecordData::Rrsig(rrsig) = r.data() else { + panic!("RRSIG expected"); + }; + let key = (owner, rrsig.type_covered()); + records = vec![r]; + if let Some(v) = iss.rrsigs.get_mut(&key) { + v.append(&mut records); + } else { + iss.rrsigs.insert(key, records); + } + } else { + let key = (owner, r.rtype()); + records = vec![r]; + if let Some(v) = iss.new_data.get_mut(&key) { + v.append(&mut records); + } else { + iss.new_data.insert(key, records); + } } } - Ok(()) + } + Ok(()) } -fn initial_diffs( iss: &mut IncrementalSigningState,) -> Result<(), Error> { +fn initial_diffs(iss: &mut IncrementalSigningState) -> Result<(), Error> { let mut new_sigs = vec![]; for (_, new_rrset) in iss.new_data.iter_mut() { - let key = (new_rrset[0].owner().clone(), new_rrset[0].rtype()); - if let Some(mut old_rrset) = iss.old_data.remove(&key) { - old_rrset.sort_by(|a, b| { - a.as_ref().data().canonical_cmp(b.as_ref().data()) - }); - new_rrset.sort_by(|a, b| { - a.as_ref().data().canonical_cmp(b.as_ref().data()) - }); - - if *old_rrset != *new_rrset { - if iss.rrsigs.remove(&key).is_some() { - sign_records(new_rrset, &iss.keys, iss.inception, iss.expiration, &mut new_sigs)?; - } - } - } - else { - if let Some((added, _)) = iss.changes.get_mut(&key.0) { - added.insert(new_rrset[0].rtype()); - } else { - let mut added = HashSet::new(); - let removed = HashSet::new(); - added.insert(new_rrset[0].rtype()); - iss.changes.insert(key.0, (added, removed)); - } - } + let key = (new_rrset[0].owner().clone(), new_rrset[0].rtype()); + if let Some(mut old_rrset) = iss.old_data.remove(&key) { + old_rrset.sort_by(|a, b| a.as_ref().data().canonical_cmp(b.as_ref().data())); + new_rrset.sort_by(|a, b| a.as_ref().data().canonical_cmp(b.as_ref().data())); + + if *old_rrset != *new_rrset { + if iss.rrsigs.remove(&key).is_some() { + sign_records( + new_rrset, + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + } + } + } else { + if let Some((added, _)) = iss.changes.get_mut(&key.0) { + added.insert(new_rrset[0].rtype()); + } else { + let mut added = HashSet::new(); + let removed = HashSet::new(); + added.insert(new_rrset[0].rtype()); + iss.changes.insert(key.0, (added, removed)); + } + } } for (sig, rtype) in new_sigs { - let key = (sig[0].owner().clone(), rtype); - iss.rrsigs.insert(key, sig); + let key = (sig[0].owner().clone(), rtype); + iss.rrsigs.insert(key, sig); } for (_, old_rrset) in &iss.old_data { - // What is left in old_data is removed. - let rtype = old_rrset[0].rtype(); - let key = (old_rrset[0].owner().clone(), rtype); - - iss.rrsigs.remove(&key); - - if let Some((_, removed)) = iss.changes.get_mut(&key.0) { - removed.insert(rtype); - } else { - let added = HashSet::new(); - let mut removed = HashSet::new(); - removed.insert(rtype); - iss.changes.insert(key.0, (added, removed)); - } + // What is left in old_data is removed. + let rtype = old_rrset[0].rtype(); + let key = (old_rrset[0].owner().clone(), rtype); + + iss.rrsigs.remove(&key); + + if let Some((_, removed)) = iss.changes.get_mut(&key.0) { + removed.insert(rtype); + } else { + let added = HashSet::new(); + let mut removed = HashSet::new(); + removed.insert(rtype); + iss.changes.insert(key.0, (added, removed)); + } } Ok(()) } -fn incremental_nsec( iss: &mut IncrementalSigningState,) -> Result<(), Error> { - // Should changes be sorted or not? If changes is sorted we will +fn incremental_nsec(iss: &mut IncrementalSigningState) -> Result<(), Error> { + // Should changes be sorted or not? If changes is sorted we will // process a new delegation before any glue. Which is more efficient. // Otherwise if glue comes first, the glue will be signed and inserted // in the NSEC chain only to be removed when the delegation is processed. @@ -2109,195 +2095,237 @@ fn incremental_nsec( iss: &mut IncrementalSigningState,) -> Result<(), Error> { let changes = iss.changes.clone(); for (key, (add, delete)) in &changes { + // The intersection between add and delete is empty. + assert!(add.intersection(delete).next().is_none()); + + if let Some(record_nsec) = iss.nsecs.get(key) { + let record_nsec = record_nsec.clone(); + let ZoneRecordData::Nsec(nsec) = record_nsec.data() else { + panic!("NSEC record expected"); + }; + + // Convert the existing RRtype bitmap into a hash set. + let mut curr = HashSet::new(); + for rtype in nsec.types() { + curr.insert(rtype); + } + + // The intersection between curr and add is empty. + assert!(curr.intersection(add).next().is_none()); + + // delete is completely contained in curr. In other words the + // difference between delete and curr is empty. + assert!(delete.difference(&curr).next().is_none()); + + if add.contains(&Rtype::NS) { + // Apex is special, but we can assume the NS RRset will not + // be added to apex. + assert!(*key != iss.origin); + + // Remove the signatures for the existing types. + for rtype in nsec.types().iter() { + // When NS is added, we should keep the signatures for + // DS and NSEC. The NSEC signature will be updated but + // there is no point in removing it first. Do no try to + // remove a signature for RRSIG because it does not exist. + if rtype == Rtype::DS || rtype == Rtype::NSEC || rtype == Rtype::RRSIG { + continue; + } + let key = (key.clone(), rtype); + iss.rrsigs.remove(&key); + } - // The intersection between add and delete is empty. - assert!(add.intersection(delete).next().is_none()); - - if let Some(record_nsec) = iss.nsecs.get(key) { - let record_nsec = record_nsec.clone(); - let ZoneRecordData::Nsec(nsec) = record_nsec.data() else { - panic!("NSEC record expected"); - }; - - // Convert the existing RRtype bitmap into a hash set. - let mut curr = HashSet::new(); - for rtype in nsec.types() { - curr.insert(rtype); - } - - // The intersection between curr and add is empty. - assert!(curr.intersection(add).next().is_none()); - - // delete is completely contained in curr. In other words the - // difference between delete and curr is empty. - assert!(delete.difference(&curr).next().is_none()); - - if add.contains(&Rtype::NS) { - // Remove the signatures for the existing types. - for rtype in nsec.types().iter() { - // When NS is added, we should keep the signatures for - // DS and NSEC. The NSEC signature will be updated but - // there is no point in removing it first. Do no try to - // remove a signature for RRSIG because it does not exist. - if rtype == Rtype::DS || rtype == Rtype::NSEC || - rtype == Rtype::RRSIG { - continue; - } - let key = (key.clone(), rtype); - iss.rrsigs.remove(&key); - } - - // Restrict curr and add to these types. - let mask: HashSet = [Rtype::NS, Rtype::DS, Rtype::NSEC, Rtype::RRSIG].into(); - - let curr: HashSet = curr.intersection(&mask).map(|r| *r).collect(); - let add: HashSet = add.intersection(&mask).map(|r| *r).collect(); - - // Update the NSEC record. - nsec_update_bitmap(&record_nsec, &nsec, &curr, &add, delete, &set_nsec_rrsig, iss); - - // Mark descendents as occluded after updating the bitmap. - // The reason is that nsec_update_bitmap uses that current - // next_name and nsec_set_occluded may change that. - nsec_set_occluded(key, iss); - - continue; - } - if delete.contains(&Rtype::NS) { - // Apex is special, but we can assume the NS RRset will not - // be removed from apex. - assert!(*key != iss.origin); - - // Curr does not include all types at this name. Add the - // missing types to curr. - let range_key = (key.clone(), 0.into()); - let range = iss.new_data.range(range_key..); - for ((r_name, r_type), _) in range { - if r_name != key { - break; - } - if add.contains(r_type) { - // Skip what we are trying to add. - continue; - } - curr.insert(*r_type); - } - - let mut new = nsec_update_bitmap(&record_nsec, &nsec, &curr, add, delete, &set_nsec_rrsig, iss); - - // Sign the types at this name except for NSEC, and RRSIG. - new.remove(&Rtype::NSEC); - new.remove(&Rtype::RRSIG); - sign_rtype_set(key, &new, iss)?; - - // Name that were previously occluded are no longer. - nsec_clear_occluded(key, iss)?; - continue; - } - if *key != iss.origin && nsec.types().contains(Rtype::NS) { - // NS marks a delegation but only when the NS is not - // at the apex. - - // If the add set contains DS then sign the DS RRset. - if add.contains(&Rtype::DS) { - let ds_set: HashSet<_> = [Rtype::DS].into(); - sign_rtype_set(key, &ds_set, iss)?; - } - nsec_update_bitmap(&record_nsec, &nsec, &curr, add, delete, &set_nsec_rrsig, iss); - continue; - } - - // The add types need to be signed. - sign_rtype_set(key, add, iss)?; - - nsec_update_bitmap(&record_nsec, &nsec, &curr, add, delete, &set_nsec_rrsig, iss); - } else { - if add.is_empty() { - assert!(!delete.is_empty()); - // No need to do anything. - continue; - } - assert!(delete.is_empty()); - if is_occluded(key, iss) { - // No need to do anything. - continue; - } - - if add.contains(&Rtype::NS) { - // Create a new NSEC record and sign only DS records (if any). - let mut rtypebitmap = RtypeBitmap::::builder(); - rtypebitmap.add(Rtype::NSEC).expect("should not fail"); - rtypebitmap.add(Rtype::RRSIG).expect("should not fail"); - for rtype in add { - rtypebitmap.add(*rtype).expect("should not fail"); - } - let rtypebitmap = rtypebitmap.finalize(); - nsec_insert(key, rtypebitmap, iss); - if add.contains(&Rtype::DS) { - let ds_set: HashSet<_> = [Rtype::DS].into(); - sign_rtype_set(key, &ds_set, iss)?; - } - - // nsec_set_occluded expects the NSEC for key to exist. - // So call this after inserting the new NSEC record. - nsec_set_occluded(key, iss); - continue; - } - // Create a new NSEC record and sign all records. - let mut rtypebitmap = RtypeBitmap::::builder(); - rtypebitmap.add(Rtype::NSEC).expect("should not fail"); - rtypebitmap.add(Rtype::RRSIG).expect("should not fail"); - for rtype in add { - rtypebitmap.add(*rtype).expect("should not fail"); - } - let rtypebitmap = rtypebitmap.finalize(); - nsec_insert(key, rtypebitmap, iss); - sign_rtype_set(key, add, iss)?; - } + // Restrict curr and add to these types. + let mask: HashSet = [Rtype::NS, Rtype::DS, Rtype::NSEC, Rtype::RRSIG].into(); + + let curr: HashSet = curr.intersection(&mask).map(|r| *r).collect(); + let add: HashSet = add.intersection(&mask).map(|r| *r).collect(); + + // Update the NSEC record. + nsec_update_bitmap( + &record_nsec, + &nsec, + &curr, + &add, + delete, + &set_nsec_rrsig, + iss, + ); + + // Mark descendents as occluded after updating the bitmap. + // The reason is that nsec_update_bitmap uses the current + // next_name and nsec_set_occluded may change that. + nsec_set_occluded(key, iss); + + continue; + } + if delete.contains(&Rtype::NS) { + // Apex is special, but we can assume the NS RRset will not + // be removed from apex. + assert!(*key != iss.origin); + + // Curr does not include all types at this name. Add the + // missing types to curr. + let range_key = (key.clone(), 0.into()); + let range = iss.new_data.range(range_key..); + for ((r_name, r_type), _) in range { + if r_name != key { + break; + } + if add.contains(r_type) { + // Skip what we are trying to add. + continue; + } + curr.insert(*r_type); + } + + let mut new = nsec_update_bitmap( + &record_nsec, + &nsec, + &curr, + add, + delete, + &set_nsec_rrsig, + iss, + ); + + // Sign the types at this name except for NSEC, and RRSIG. + new.remove(&Rtype::NSEC); + new.remove(&Rtype::RRSIG); + sign_rtype_set(key, &new, iss)?; + + // Names that were previously occluded are no longer. + nsec_clear_occluded(key, iss)?; + continue; + } + if *key != iss.origin && nsec.types().contains(Rtype::NS) { + // NS marks a delegation but only when the NS is not + // at the apex. + + // If the add set contains DS then sign the DS RRset. + if add.contains(&Rtype::DS) { + let ds_set: HashSet<_> = [Rtype::DS].into(); + sign_rtype_set(key, &ds_set, iss)?; + } + nsec_update_bitmap( + &record_nsec, + &nsec, + &curr, + add, + delete, + &set_nsec_rrsig, + iss, + ); + continue; + } + + // The add types need to be signed. + sign_rtype_set(key, add, iss)?; + + nsec_update_bitmap( + &record_nsec, + &nsec, + &curr, + add, + delete, + &set_nsec_rrsig, + iss, + ); + } else { + if add.is_empty() { + assert!(!delete.is_empty()); + // No need to do anything. + continue; + } + assert!(delete.is_empty()); + if is_occluded(key, iss) { + // No need to do anything. + continue; + } + + if add.contains(&Rtype::NS) { + // Create a new NSEC record and sign only DS records (if any). + let rtypebitmap = rtypebitmap_from_iterator(add.iter()); + nsec_insert(key, rtypebitmap, iss); + if add.contains(&Rtype::DS) { + let ds_set: HashSet<_> = [Rtype::DS].into(); + sign_rtype_set(key, &ds_set, iss)?; + } + + // nsec_set_occluded expects the NSEC for key to exist. + // So call this after inserting the new NSEC record. + nsec_set_occluded(key, iss); + continue; + } + // Create a new NSEC record and sign all records. + let rtypebitmap = rtypebitmap_from_iterator(add.iter()); + nsec_insert(key, rtypebitmap, iss); + sign_rtype_set(key, add, iss)?; + } } Ok(()) } -fn nsec_insert(name: &Name, rtypebitmap: RtypeBitmap, iss: &mut IncrementalSigningState) { +fn nsec_insert( + name: &Name, + rtypebitmap: RtypeBitmap, + iss: &mut IncrementalSigningState, +) { // Try to find the NSEC record that comes before the one we are trying - // to insert. Assume that the APEX NSEC will always exist can sort + // to insert. Assume that the APEX NSEC will always exist can sort // before anything else. let mut range = iss.nsecs.range::, _>(..name); - let (previous_name, previous_record) = range.next_back().expect("previous NSEC record should exist"); + let (previous_name, previous_record) = range + .next_back() + .expect("previous NSEC record should exist"); let previous_name = previous_name.clone(); let previous_record = previous_record.clone(); drop(range); let ZoneRecordData::Nsec(previous_nsec) = previous_record.data() else { - panic!("NSEC record expected"); + panic!("NSEC record expected"); }; let next = previous_nsec.next_name(); let new_nsec = Nsec::new(next.clone(), rtypebitmap); - let new_record = Record::new(name.clone(), previous_record.class(), - previous_record.ttl(), ZoneRecordData::Nsec(new_nsec)); + let new_record = Record::new( + name.clone(), + previous_record.class(), + previous_record.ttl(), + ZoneRecordData::Nsec(new_nsec), + ); iss.nsecs.insert(name.clone(), new_record); iss.modified_nsecs.insert(name.clone()); let previous_nsec = Nsec::new(name.clone(), previous_nsec.types().clone()); - let previous_record = Record::new(previous_name.clone(), previous_record.class(), - previous_record.ttl(), ZoneRecordData::Nsec(previous_nsec)); + let previous_record = Record::new( + previous_name.clone(), + previous_record.class(), + previous_record.ttl(), + ZoneRecordData::Nsec(previous_nsec), + ); iss.nsecs.insert(previous_name.clone(), previous_record); iss.modified_nsecs.insert(previous_name.clone()); } fn nsec_remove(name: &Name, next_name: &Name, iss: &mut IncrementalSigningState) { // Try to find the NSEC record that comes before the one we are trying - // to remove. Assume that the APEX NSEC will always exist can sort + // to remove. Assume that the APEX NSEC will always exist can sort // before anything else. let mut range = iss.nsecs.range::, _>(..name); - let (previous_name, previous_record) = range.next_back().expect("previous NSEC record should exist"); + let (previous_name, previous_record) = range + .next_back() + .expect("previous NSEC record should exist"); let previous_name = previous_name.clone(); let previous_record = previous_record.clone(); drop(range); let ZoneRecordData::Nsec(previous_nsec) = previous_record.data() else { - panic!("NSEC record expected"); + panic!("NSEC record expected"); }; let previous_nsec = Nsec::new(next_name.clone(), previous_nsec.types().clone()); - let previous_record = Record::new(previous_name.clone(), previous_record.class(), - previous_record.ttl(), ZoneRecordData::Nsec(previous_nsec)); + let previous_record = Record::new( + previous_name.clone(), + previous_record.class(), + previous_record.ttl(), + ZoneRecordData::Nsec(previous_nsec), + ); iss.nsecs.insert(previous_name.clone(), previous_record); iss.modified_nsecs.insert(previous_name.clone()); iss.nsecs.remove(name); @@ -2307,24 +2335,33 @@ fn nsec_remove(name: &Name, next_name: &Name, iss: &mut Incrementa } // Return the effective result HashSet even when the NSEC record gets deleted. -fn nsec_update_bitmap(record: &ZRD, nsec: &Nsec>, curr: &HashSet, add: &HashSet, delete: &HashSet, set_nsec_rrsig: &HashSet, iss: &mut IncrementalSigningState) -> HashSet { +fn nsec_update_bitmap( + record: &ZRD, + nsec: &Nsec>, + curr: &HashSet, + add: &HashSet, + delete: &HashSet, + set_nsec_rrsig: &HashSet, + iss: &mut IncrementalSigningState, +) -> HashSet { // Update curr. let curr: HashSet<_> = curr.union(add).map(|t| *t).collect(); let curr: HashSet<_> = curr.difference(delete).map(|t| *t).collect(); let owner = record.owner(); if curr == *set_nsec_rrsig { - nsec_remove(owner, nsec.next_name(), iss); - return curr; + nsec_remove(owner, nsec.next_name(), iss); + return curr; } - let mut builder = RtypeBitmap::::builder(); - for rtype in &curr { - builder.add(*rtype).expect("should not fail"); - } - let nsec = Nsec::new(nsec.next_name().clone(), builder.finalize()); - let record = Record::new(record.owner().clone(), - record.class(), record.ttl(), ZoneRecordData::Nsec(nsec)); + let rtypebitmap = rtypebitmap_from_iterator(curr.iter()); + let nsec = Nsec::new(nsec.next_name().clone(), rtypebitmap); + let record = Record::new( + record.owner().clone(), + record.class(), + record.ttl(), + ZoneRecordData::Nsec(nsec), + ); iss.nsecs.insert(owner.clone(), record); iss.modified_nsecs.insert(owner.clone()); @@ -2333,39 +2370,39 @@ fn nsec_update_bitmap(record: &ZRD, nsec: &Nsec>, curr: &Hash fn nsec_set_occluded(name: &Name, iss: &mut IncrementalSigningState) { let Some(nsec_record) = iss.nsecs.get(name) else { - panic!("NSEC for {name} expected to exist"); + panic!("NSEC for {name} expected to exist"); }; let ZoneRecordData::Nsec(nsec) = nsec_record.data() else { - panic!("NSEC record expected"); + panic!("NSEC record expected"); }; let nsec = nsec.clone(); let mut next = nsec.next_name().clone(); loop { - if !next.ends_with(name) { - break; - } - - // For consistency, make sure next is not equal to name. - if next == name { - break; - } - let curr = next; - let Some(nsec_record) = iss.nsecs.get(&curr) else { - panic!("NSEC for {name} expected to exist"); - }; - let ZoneRecordData::Nsec(nsec) = nsec_record.data() else { - panic!("NSEC record expected"); - }; - let nsec = nsec.clone(); - next = nsec.next_name().clone(); - - nsec_remove(&curr, &next, iss); - - // Remove all signatures. - for rtype in nsec.types().iter() { - let key = (curr.clone(), rtype); - iss.rrsigs.remove(&key); - } + if !next.ends_with(name) { + break; + } + + // For consistency, make sure next is not equal to name. + if next == name { + break; + } + let curr = next; + let Some(nsec_record) = iss.nsecs.get(&curr) else { + panic!("NSEC for {name} expected to exist"); + }; + let ZoneRecordData::Nsec(nsec) = nsec_record.data() else { + panic!("NSEC record expected"); + }; + let nsec = nsec.clone(); + next = nsec.next_name().clone(); + + nsec_remove(&curr, &next, iss); + + // Remove all signatures. + for rtype in nsec.types().iter() { + let key = (curr.clone(), rtype); + iss.rrsigs.remove(&key); + } } } @@ -2380,65 +2417,59 @@ fn nsec_clear_occluded(name: &Name, iss: &mut IncrementalSigningState) -> let mut delegation: Option> = None; for ((key_name, key_rtype), _) in range { - // There is no easy way to avoid name showing up in the range. Just - // filter out name. - if key_name == name { - continue; - } - - // Make sure curr_name is below name. - if !key_name.ends_with(name) { - break; - } - if let Some(d) = &delegation { - if key_name.ends_with(d) && key_name != d { - // Skip. - continue; - } - } - if *key_rtype == Rtype::NS { - // Set key_name as a delegation. - delegation = Some(key_name.clone()); - } - if let Some(curr_name) = opt_curr_name { - if key_name == curr_name { - curr_types.insert(*key_rtype); - } else { - work.push((curr_name.clone(), curr_types)); - opt_curr_name = Some(key_name); - curr_types = [*key_rtype].into(); - } - } else { - opt_curr_name = Some(key_name); - curr_types.insert(*key_rtype); - } + // There is no easy way to avoid name showing up in the range. Just + // filter out name. + if key_name == name { + continue; + } + + // Make sure curr_name is below name. + if !key_name.ends_with(name) { + break; + } + if let Some(d) = &delegation { + if key_name.ends_with(d) && key_name != d { + // Skip. + continue; + } + } + if *key_rtype == Rtype::NS { + // Set key_name as a delegation. + delegation = Some(key_name.clone()); + } + if let Some(curr_name) = opt_curr_name { + if key_name == curr_name { + curr_types.insert(*key_rtype); + } else { + work.push((curr_name.clone(), curr_types)); + opt_curr_name = Some(key_name); + curr_types = [*key_rtype].into(); + } + } else { + opt_curr_name = Some(key_name); + curr_types.insert(*key_rtype); + } } if let Some(curr_name) = opt_curr_name { - work.push((curr_name.clone(), curr_types)); + work.push((curr_name.clone(), curr_types)); } for (curr_name, curr_types) in work { - let mut curr_types = if curr_types.contains(&Rtype::NS) { - let has_ds = curr_types.contains(&Rtype::DS); - let mut curr_types: HashSet = [Rtype::NS].into(); - if has_ds { - curr_types.insert(Rtype::DS); - } - curr_types - } else { - curr_types - }; - let mut rtypebitmap = RtypeBitmap::::builder(); - rtypebitmap.add(Rtype::NSEC).expect("should not fail"); - rtypebitmap.add(Rtype::RRSIG).expect("should not fail"); - for rtype in &curr_types { - rtypebitmap.add(*rtype).expect("should not fail"); - } - let rtypebitmap = rtypebitmap.finalize(); - - // Make sure NS doesn't get signed. - curr_types.remove(&Rtype::NS); - sign_rtype_set(&curr_name, &curr_types, iss)?; - nsec_insert(&curr_name, rtypebitmap, iss); + let mut curr_types = if curr_types.contains(&Rtype::NS) { + let has_ds = curr_types.contains(&Rtype::DS); + let mut curr_types: HashSet = [Rtype::NS].into(); + if has_ds { + curr_types.insert(Rtype::DS); + } + curr_types + } else { + curr_types + }; + let rtypebitmap = rtypebitmap_from_iterator(curr_types.iter()); + + // Make sure NS doesn't get signed. + curr_types.remove(&Rtype::NS); + sign_rtype_set(&curr_name, &curr_types, iss)?; + nsec_insert(&curr_name, rtypebitmap, iss); } Ok(()) } @@ -2447,74 +2478,101 @@ fn is_occluded(name: &Name, iss: &IncrementalSigningState) -> bool { // We need to check if the parent of name is a delegation. Stop // when we reached origin. let Some(mut curr) = name.parent() else { - // We asked for the parent of the root. That is weird. Just - // return no occluded. - return false; + // We asked for the parent of the root. That is weird. Just + // return no occluded. + return false; }; loop { - if curr == iss.origin { - // We reached apex. The name was not occluded. - return false; - } - if !curr.ends_with(&iss.origin) { - // Something weird is going on. Return not occluded. - return false; - } - if iss.new_data.get(&(curr.clone(), Rtype::NS)).is_some() { - // Name is occluded. - return true; - } - let Some(parent) = curr.parent() else { - // We asked for the parent of the root. That is weird. Just - // return no occluded. - return false; - }; - curr = parent; + if curr == iss.origin { + // We reached apex. The name was not occluded. + return false; + } + if !curr.ends_with(&iss.origin) { + // Something weird is going on. Return not occluded. + return false; + } + if iss.new_data.get(&(curr.clone(), Rtype::NS)).is_some() { + // Name is occluded. + return true; + } + let Some(parent) = curr.parent() else { + // We asked for the parent of the root. That is weird. Just + // return no occluded. + return false; + }; + curr = parent; } } -fn sign_rtype_set(name: &Name, set: &HashSet, iss: &mut IncrementalSigningState) -> Result<(), Error> { +fn sign_rtype_set( + name: &Name, + set: &HashSet, + iss: &mut IncrementalSigningState, +) -> Result<(), Error> { let mut new_sigs = vec![]; for rtype in set { - let key = (name.clone(), rtype.clone()); - let Some(records) = iss.new_data.get(&key) else { - panic!("Expected something for {}/{}", name, rtype); - }; - sign_records(records, &iss.keys, iss.inception, iss.expiration, &mut new_sigs)?; + let key = (name.clone(), rtype.clone()); + let Some(records) = iss.new_data.get(&key) else { + panic!("Expected something for {}/{}", name, rtype); + }; + sign_records( + records, + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; } for (sig, rtype) in new_sigs { - let key = (sig[0].owner().clone(), rtype); - iss.rrsigs.insert(key, sig); + let key = (sig[0].owner().clone(), rtype); + iss.rrsigs.insert(key, sig); } Ok(()) } -fn sign_records(records: &[ZRD], keys: &[SigningKey], inception: Timestamp, expiration: Timestamp, new_sigs: &mut Vec<(Vec, Rtype)>) -> Result<(), Error> { - +fn sign_records( + records: &[ZRD], + keys: &[SigningKey], + inception: Timestamp, + expiration: Timestamp, + new_sigs: &mut Vec<(Vec, Rtype)>, +) -> Result<(), Error> { let rtype = records[0].rtype(); - if rtype == Rtype::DNSKEY || rtype == Rtype::CDS || - rtype == Rtype::CDNSKEY { - // These records get signed with the KSK(s). Don't touch - // the signatures. - return Ok(()); + if rtype == Rtype::DNSKEY || rtype == Rtype::CDS || rtype == Rtype::CDNSKEY { + // These records get signed with the KSK(s). Don't touch + // the signatures. + return Ok(()); } - let rrset = Rrset::new(records) - .map_err(|e| format!("Rrset::new failed: {e}"))?; + let rrset = Rrset::new(records).map_err(|e| format!("Rrset::new failed: {e}"))?; let mut rrsig_records = vec![]; for key in keys { - let rrsig = sign_rrset(key, &rrset, - inception, - expiration) - .map_err(|e| format!("signing failed: {e}"))?; - let record = Record::new(rrsig.owner().clone(), - rrsig.class(), rrsig.ttl(), ZoneRecordData::Rrsig(rrsig.data().clone())); - rrsig_records.push(record); + let rrsig = sign_rrset(key, &rrset, inception, expiration) + .map_err(|e| format!("signing failed: {e}"))?; + let record = Record::new( + rrsig.owner().clone(), + rrsig.class(), + rrsig.ttl(), + ZoneRecordData::Rrsig(rrsig.data().clone()), + ); + rrsig_records.push(record); } new_sigs.push((rrsig_records, rrset.rtype())); Ok(()) } +fn rtypebitmap_from_iterator<'a, I>(iter: I) -> RtypeBitmap +where + I: Iterator, +{ + let mut rtypebitmap = RtypeBitmap::::builder(); + rtypebitmap.add(Rtype::NSEC).expect("should not fail"); + rtypebitmap.add(Rtype::RRSIG).expect("should not fail"); + for rtype in iter { + rtypebitmap.add(*rtype).expect("should not fail"); + } + rtypebitmap.finalize() +} //------------ SigningMode --------------------------------------------------- @@ -2828,26 +2886,3 @@ impl std::ops::Deref for Nsec3HashMap { &self.hashes_by_unhashed_owner } } - -//------------ TestableTimestamp --------------------------------------------- - -struct TestableTimestamp; - -impl TestableTimestamp { - fn now() -> Timestamp { - if cfg!(test) { - // Don't use Timestamp::now() because that will use the actual - // SystemTime::now() even in tests which, if there are any - // unexpected delays as can happen in a CI environment, can cause - // two nearby calls to Timestamp::now() to return a different - // number of seconds since the epoch which will thus fail to - // compare as equal in a test. Ironically the underlying Timestamp - // implementation supports mocking of time, but the test flag is - // not set by Cargo for dependencies, only for our own code, so we - // have to manually construct a predictable Timestamp ourselves. - Timestamp::from(0) - } else { - Timestamp::now() - } - } -} From 6304a9db151d153cbb2ce9444e4e4e1a06bbdcd4 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Thu, 15 Jan 2026 15:01:11 +0100 Subject: [PATCH 06/20] Clippy --- src/commands/signer.rs | 88 ++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 54 deletions(-) diff --git a/src/commands/signer.rs b/src/commands/signer.rs index 4e57660b..73706cd5 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -1733,10 +1733,10 @@ type ChangesValue = (RtypeSet, RtypeSet); // add set followed by delete set. struct IncrementalSigningState { origin: Name, - old_data: HashMap<(Name, Rtype), Vec>, - new_data: BTreeMap<(Name, Rtype), Vec>, - nsecs: BTreeMap, ZRD>, - rrsigs: HashMap<(Name, Rtype), Vec>, + old_data: HashMap<(Name, Rtype), Vec>, + new_data: BTreeMap<(Name, Rtype), Vec>, + nsecs: BTreeMap, Zrd>, + rrsigs: HashMap<(Name, Rtype), Vec>, changes: HashMap, ChangesValue>, modified_nsecs: HashSet>, @@ -1767,7 +1767,7 @@ impl IncrementalSigningState { } } -type ZRD = Record, ZoneRecordData>>; +type Zrd = Record, ZoneRecordData>>; fn load_signed_zone(iss: &mut IncrementalSigningState, path: &PathBuf) -> Result<(), Error> { // Don't use Zonefile::load() as it knows nothing about the size of @@ -2038,33 +2038,29 @@ fn initial_diffs(iss: &mut IncrementalSigningState) -> Result<(), Error> { old_rrset.sort_by(|a, b| a.as_ref().data().canonical_cmp(b.as_ref().data())); new_rrset.sort_by(|a, b| a.as_ref().data().canonical_cmp(b.as_ref().data())); - if *old_rrset != *new_rrset { - if iss.rrsigs.remove(&key).is_some() { - sign_records( - new_rrset, - &iss.keys, - iss.inception, - iss.expiration, - &mut new_sigs, - )?; - } + if *old_rrset != *new_rrset && iss.rrsigs.remove(&key).is_some() { + sign_records( + new_rrset, + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; } + } else if let Some((added, _)) = iss.changes.get_mut(&key.0) { + added.insert(new_rrset[0].rtype()); } else { - if let Some((added, _)) = iss.changes.get_mut(&key.0) { - added.insert(new_rrset[0].rtype()); - } else { - let mut added = HashSet::new(); - let removed = HashSet::new(); - added.insert(new_rrset[0].rtype()); - iss.changes.insert(key.0, (added, removed)); - } + let mut added = HashSet::new(); + let removed = HashSet::new(); + added.insert(new_rrset[0].rtype()); + iss.changes.insert(key.0, (added, removed)); } } for (sig, rtype) in new_sigs { let key = (sig[0].owner().clone(), rtype); iss.rrsigs.insert(key, sig); } - for (_, old_rrset) in &iss.old_data { + for old_rrset in iss.old_data.values() { // What is left in old_data is removed. let rtype = old_rrset[0].rtype(); let key = (old_rrset[0].owner().clone(), rtype); @@ -2138,13 +2134,13 @@ fn incremental_nsec(iss: &mut IncrementalSigningState) -> Result<(), Error> { // Restrict curr and add to these types. let mask: HashSet = [Rtype::NS, Rtype::DS, Rtype::NSEC, Rtype::RRSIG].into(); - let curr: HashSet = curr.intersection(&mask).map(|r| *r).collect(); - let add: HashSet = add.intersection(&mask).map(|r| *r).collect(); + let curr = curr.intersection(&mask).copied().collect(); + let add = add.intersection(&mask).copied().collect(); // Update the NSEC record. nsec_update_bitmap( &record_nsec, - &nsec, + nsec, &curr, &add, delete, @@ -2181,7 +2177,7 @@ fn incremental_nsec(iss: &mut IncrementalSigningState) -> Result<(), Error> { let mut new = nsec_update_bitmap( &record_nsec, - &nsec, + nsec, &curr, add, delete, @@ -2207,30 +2203,14 @@ fn incremental_nsec(iss: &mut IncrementalSigningState) -> Result<(), Error> { let ds_set: HashSet<_> = [Rtype::DS].into(); sign_rtype_set(key, &ds_set, iss)?; } - nsec_update_bitmap( - &record_nsec, - &nsec, - &curr, - add, - delete, - &set_nsec_rrsig, - iss, - ); + nsec_update_bitmap(&record_nsec, nsec, &curr, add, delete, &set_nsec_rrsig, iss); continue; } // The add types need to be signed. sign_rtype_set(key, add, iss)?; - nsec_update_bitmap( - &record_nsec, - &nsec, - &curr, - add, - delete, - &set_nsec_rrsig, - iss, - ); + nsec_update_bitmap(&record_nsec, nsec, &curr, add, delete, &set_nsec_rrsig, iss); } else { if add.is_empty() { assert!(!delete.is_empty()); @@ -2336,7 +2316,7 @@ fn nsec_remove(name: &Name, next_name: &Name, iss: &mut Incrementa // Return the effective result HashSet even when the NSEC record gets deleted. fn nsec_update_bitmap( - record: &ZRD, + record: &Zrd, nsec: &Nsec>, curr: &HashSet, add: &HashSet, @@ -2345,8 +2325,8 @@ fn nsec_update_bitmap( iss: &mut IncrementalSigningState, ) -> HashSet { // Update curr. - let curr: HashSet<_> = curr.union(add).map(|t| *t).collect(); - let curr: HashSet<_> = curr.difference(delete).map(|t| *t).collect(); + let curr: HashSet<_> = curr.union(add).copied().collect(); + let curr = curr.difference(delete).copied().collect(); let owner = record.owner(); if curr == *set_nsec_rrsig { @@ -2491,7 +2471,7 @@ fn is_occluded(name: &Name, iss: &IncrementalSigningState) -> bool { // Something weird is going on. Return not occluded. return false; } - if iss.new_data.get(&(curr.clone(), Rtype::NS)).is_some() { + if iss.new_data.contains_key(&(curr.clone(), Rtype::NS)) { // Name is occluded. return true; } @@ -2511,9 +2491,9 @@ fn sign_rtype_set( ) -> Result<(), Error> { let mut new_sigs = vec![]; for rtype in set { - let key = (name.clone(), rtype.clone()); + let key = (name.clone(), *rtype); let Some(records) = iss.new_data.get(&key) else { - panic!("Expected something for {}/{}", name, rtype); + panic!("Expected something for {name}/{rtype}"); }; sign_records( records, @@ -2531,11 +2511,11 @@ fn sign_rtype_set( } fn sign_records( - records: &[ZRD], + records: &[Zrd], keys: &[SigningKey], inception: Timestamp, expiration: Timestamp, - new_sigs: &mut Vec<(Vec, Rtype)>, + new_sigs: &mut Vec<(Vec, Rtype)>, ) -> Result<(), Error> { let rtype = records[0].rtype(); if rtype == Rtype::DNSKEY || rtype == Rtype::CDS || rtype == Rtype::CDNSKEY { From cea21ada634f70b295c14d5e80ef9b30cf70f42b Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Mon, 2 Feb 2026 17:57:28 +0100 Subject: [PATCH 07/20] Passes test 1 for NSEC3. --- Cargo.lock | 2 - Cargo.toml | 3 + src/commands/signer.rs | 863 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 778 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e7098e4..cb787d59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -358,7 +358,6 @@ dependencies = [ [[package]] name = "domain" version = "0.11.1" -source = "git+https://github.com/NLnetLabs/domain.git?branch=main#f27a0255ddc0e2098b4c17aa310ea4253743507a" dependencies = [ "arc-swap", "bumpalo", @@ -404,7 +403,6 @@ dependencies = [ [[package]] name = "domain-macros" version = "0.11.1" -source = "git+https://github.com/NLnetLabs/domain.git?branch=main#f27a0255ddc0e2098b4c17aa310ea4253743507a" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 4398abb8..40525bcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,3 +142,6 @@ assets = [ # Set Obsoletes per https://docs.fedoraproject.org/en-US/packaging-guidelines/#renaming-or-replacing-existing-packages. [package.metadata.generate-rpm.obsoletes] ldns-utils = "< 0:1.8.4-2" + +[patch.'https://github.com/NLnetLabs/domain.git'] +domain = { path = "../domain" } diff --git a/src/commands/signer.rs b/src/commands/signer.rs index 73706cd5..c1f124d8 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -3,13 +3,16 @@ use super::nsec3hash::Nsec3Hash; use crate::env::{Env, Stream}; use crate::error::{Context, Error}; use crate::DISPLAY_KIND; -use bytes::{BufMut, Bytes}; +use bytes::{BytesMut, BufMut, Bytes}; use clap::builder::ValueParser; use clap::Subcommand; use core::clone::Clone; use core::cmp::Ordering; use core::fmt::Write; use core::str::FromStr; +use domain::rdata::nsec3::OwnerHash; +use domain::utils::base32; +use domain::dnssec::common::nsec3_hash; use domain::base::iana::nsec3::Nsec3HashAlgorithm; use domain::base::iana::zonemd::{ZonemdAlgorithm, ZonemdScheme}; use domain::base::iana::Class; @@ -46,12 +49,13 @@ use octseq::builder::with_infallible; use rayon::slice::ParallelSliceMut; use ring::digest; use serde::{Deserialize, Serialize}; +use std::time::Instant; use std::cmp::min; use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt::{self, Display}; use std::fs::{metadata, File}; use std::io::Write as IoWrite; -use std::io::{self, stdout, BufWriter}; +use std::io::{self, /*stdout,*/ BufWriter}; use std::path::{Path, PathBuf}; use std::process::Command; use std::time::UNIX_EPOCH; @@ -1223,42 +1227,121 @@ impl Signer { iss.keys = keys; + let start = Instant::now(); load_signed_zone(&mut iss, &sc.zonefile_out).unwrap(); - - // Should check if the NSEC(3) mode has changed. Sign the full - // zone if that has happened. - + println!("loading signed zone took {:?}", start.elapsed()); + + // Note that we could try to regenerate the NSEC(3). Assume that + // switching between NSEC, NSEC3, and NSEC3 opt-out (or other NSEC3 + // parameter changes) is rare enough that we can just resign the full + // zone. + let key = (iss.origin.clone(), Rtype::NSEC3PARAM); + let opt_nsec3param = iss.old_data.get(&key); + if let Some(nsec3param_records) = opt_nsec3param { + // Zone was signed with NSEC3. + if !sc.use_nsec3 { + // Zone is signed with NSEC3 but we want NSEC. + todo!(); + } + let ZoneRecordData::Nsec3param(nsec3param) = nsec3param_records[0].data() else { + panic!("ZoneRecordData::Nsec3param expected"); + }; + if nsec3param.hash_algorithm() != sc.algorithm || + nsec3param.opt_out_flag() != sc.opt_out || + nsec3param.iterations() != sc.iterations || + *nsec3param.salt() != sc.salt { + // Parameters changed, resign. + todo!(); + } + + // Nothing has changed. Insert the old NSEC3PARAM records in the + // new zone data. + iss.new_data.insert(key, nsec3param_records.to_vec()); + } else { + // Zone was signed with NSEC, check if that is also the target. + if sc.use_nsec3 { + // Resign the full zone with NSEC3. + todo!(); + } + + // Stay with NSEC. + } + + let start = Instant::now(); load_unsigned_zone(&mut iss, &sc.zonefile_in).unwrap(); + println!("loading new unsigned zone took {:?}", start.elapsed()); + let start = Instant::now(); load_apex_records(kss, &mut iss)?; initial_diffs(&mut iss)?; - // Should check whether to do NSEC, NSEC3, or NSEC3 opt-out. Just do - // NSEC for now. - incremental_nsec(&mut iss)?; + if sc.use_nsec3 { + if sc.opt_out { + todo!(); + } else { + incremental_nsec3(&mut iss)?; + } + } + else { + incremental_nsec(&mut iss)?; + } let mut new_sigs = vec![]; - for m in &iss.modified_nsecs { - let Some(nsec) = iss.nsecs.get(m) else { - panic!("NSEC for {m} should exist"); - }; - - let nsec = nsec.clone(); - sign_records( - &[nsec], - &iss.keys, - iss.inception, - iss.expiration, - &mut new_sigs, - )?; - } + if sc.use_nsec3 { + for m in &iss.modified_nsecs { + let Some(nsec3) = iss.nsec3s.get(m) else { + panic!("NSEC3 for {m} should exist"); + }; + + let nsec3 = nsec3.clone(); + sign_records( + &[nsec3], + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + } + } else { + for m in &iss.modified_nsecs { + let Some(nsec) = iss.nsecs.get(m) else { + panic!("NSEC for {m} should exist"); + }; + + let nsec = nsec.clone(); + sign_records( + &[nsec], + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + } + } for (sig, rtype) in new_sigs { let key = (sig[0].owner().clone(), rtype); iss.rrsigs.insert(key, sig); } + println!("incremental signing took {:?}", start.elapsed()); +/* let mut writer = stdout(); +*/ + + let start = Instant::now(); + let mut writer = { + let filename = sc.zonefile_out.as_os_str().to_str().unwrap(); + let filename = filename.to_owned() + "-incremental"; + let file = File::create(&filename).map_err(|e| { + format!( + "unable to create file {filename}: {e}", + ) + })?; + BufWriter::new(file) + // FileOrStdout::File(file) + }; + for (_, data) in iss.new_data { for rr in data { writer @@ -1271,6 +1354,11 @@ impl Signer { .write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))) .map_err(|e| format!("unable write signed zone: {e}"))?; } + for (_, rr) in iss.nsec3s { + writer + .write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))) + .map_err(|e| format!("unable write signed zone: {e}"))?; + } for (_, data) in iss.rrsigs { for rr in data { let ZoneRecordData::Rrsig(rrsig) = rr.data() else { @@ -1282,6 +1370,7 @@ impl Signer { .map_err(|e| format!("unable write signed zone: {e}"))?; } } + println!("writing output took {:?}", start.elapsed()); todo!(); } @@ -1736,6 +1825,7 @@ struct IncrementalSigningState { old_data: HashMap<(Name, Rtype), Vec>, new_data: BTreeMap<(Name, Rtype), Vec>, nsecs: BTreeMap, Zrd>, + nsec3s: BTreeMap, Zrd>, rrsigs: HashMap<(Name, Rtype), Vec>, changes: HashMap, ChangesValue>, @@ -1743,6 +1833,9 @@ struct IncrementalSigningState { keys: Vec>, inception: Timestamp, expiration: Timestamp, + + // NSEC3 paramters. + nsec3param: Nsec3param, } impl IncrementalSigningState { @@ -1752,17 +1845,26 @@ impl IncrementalSigningState { let inception = (now - sc.inception_offset.as_secs() as u32).into(); let expiration = (now + sc.signature_lifetime.as_secs() as u32).into(); + // This is the only way to deal with opt-out. There is no data type + // for flags or constant for opt-out. Creating an Nsec3param makes it + // possible to set opt-out. + let mut nsec3param = Nsec3param::new(sc.algorithm, 0, sc.iterations, sc.salt.clone()); + if sc.opt_out { + nsec3param.set_opt_out_flag(); + } Self { origin, old_data: HashMap::new(), new_data: BTreeMap::new(), nsecs: BTreeMap::new(), + nsec3s: BTreeMap::new(), rrsigs: HashMap::new(), changes: HashMap::new(), modified_nsecs: HashSet::new(), keys: vec![], inception, expiration, + nsec3param, } } } @@ -1825,10 +1927,14 @@ fn load_signed_zone(iss: &mut IncrementalSigningState, path: &PathBuf) -> Result } ZoneRecordData::Nsec(_) => { // Assume (at most) one NSEC record per owner name. - // Directly insert into the hash map. + // Directly insert into the btree map. iss.nsecs.insert(record.owner().clone(), record); } - ZoneRecordData::Nsec3(_) => todo!(), + ZoneRecordData::Nsec3(_) => { + // Assume (at most) one NSEC3 record per owner name. + // Directly insert into the btree map. + iss.nsec3s.insert(record.owner().clone(), record); + } _ => { if records.is_empty() { records.push(record); @@ -2035,6 +2141,13 @@ fn initial_diffs(iss: &mut IncrementalSigningState) -> Result<(), Error> { for (_, new_rrset) in iss.new_data.iter_mut() { let key = (new_rrset[0].owner().clone(), new_rrset[0].rtype()); if let Some(mut old_rrset) = iss.old_data.remove(&key) { + let rtype = new_rrset[0].rtype(); + if rtype == Rtype::DNSKEY || rtype == Rtype::CDS || + rtype == Rtype::CDNSKEY { + // These types are signed by the key manager. No need to + // check for changes. + continue; + } old_rrset.sort_by(|a, b| a.as_ref().data().canonical_cmp(b.as_ref().data())); new_rrset.sort_by(|a, b| a.as_ref().data().canonical_cmp(b.as_ref().data())); @@ -2225,7 +2338,7 @@ fn incremental_nsec(iss: &mut IncrementalSigningState) -> Result<(), Error> { if add.contains(&Rtype::NS) { // Create a new NSEC record and sign only DS records (if any). - let rtypebitmap = rtypebitmap_from_iterator(add.iter()); + let rtypebitmap = nsec_rtypebitmap_from_iterator(add.iter()); nsec_insert(key, rtypebitmap, iss); if add.contains(&Rtype::DS) { let ds_set: HashSet<_> = [Rtype::DS].into(); @@ -2238,7 +2351,7 @@ fn incremental_nsec(iss: &mut IncrementalSigningState) -> Result<(), Error> { continue; } // Create a new NSEC record and sign all records. - let rtypebitmap = rtypebitmap_from_iterator(add.iter()); + let rtypebitmap = nsec_rtypebitmap_from_iterator(add.iter()); nsec_insert(key, rtypebitmap, iss); sign_rtype_set(key, add, iss)?; } @@ -2246,6 +2359,195 @@ fn incremental_nsec(iss: &mut IncrementalSigningState) -> Result<(), Error> { Ok(()) } +fn incremental_nsec3(iss: &mut IncrementalSigningState) -> Result<(), Error> { + // Should changes be sorted or not? If changes is sorted we will + // process a new delegation before any glue. Which is more efficient. + // Otherwise if glue comes first, the glue will be signed and inserted + // in the NSEC chain only to be removed when the delegation is processed. + // However, we removing a delegation, the situation is reversed. For now + // assuming that sorting is not necessary. + + let changes = iss.changes.clone(); + for (key, (add, delete)) in &changes { + // The intersection between add and delete is empty. + assert!(add.intersection(delete).next().is_none()); + + let nsec3_hash_octets = + OwnerHash::::octets_from( + nsec3_hash::<_, _, BytesMut>(key, + iss.nsec3param.hash_algorithm(), + iss.nsec3param.iterations(), + iss.nsec3param.salt()) + .expect("should not fail") + ); + let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); + let mut builder = NameBuilder::::new(); + builder.append_label(nsec3_hash_base32.as_bytes()).expect("should not fail"); + let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); + if let Some(record_nsec3) = iss.nsec3s.get(&nsec3_name) { + let record_nsec3 = record_nsec3.clone(); + let ZoneRecordData::Nsec3(nsec3) = record_nsec3.data() else { + panic!("NSEC3 record expected"); + }; + + // Convert the existing RRtype bitmap into a hash set. + let mut curr = HashSet::new(); + for rtype in nsec3.types() { + curr.insert(rtype); + } + + // The intersection between curr and add is empty. + assert!(curr.intersection(add).next().is_none()); + + // delete is completely contained in curr. In other words the + // difference between delete and curr is empty. + assert!(delete.difference(&curr).next().is_none()); + + if add.contains(&Rtype::NS) { + todo!(); +/* + // Apex is special, but we can assume the NS RRset will not + // be added to apex. + assert!(*key != iss.origin); + + // Remove the signatures for the existing types. + for rtype in nsec.types().iter() { + // When NS is added, we should keep the signatures for + // DS and NSEC. The NSEC signature will be updated but + // there is no point in removing it first. Do no try to + // remove a signature for RRSIG because it does not exist. + if rtype == Rtype::DS || rtype == Rtype::NSEC || rtype == Rtype::RRSIG { + continue; + } + let key = (key.clone(), rtype); + iss.rrsigs.remove(&key); + } + + // Restrict curr and add to these types. + let mask: HashSet = [Rtype::NS, Rtype::DS, Rtype::NSEC, Rtype::RRSIG].into(); + + let curr = curr.intersection(&mask).copied().collect(); + let add = add.intersection(&mask).copied().collect(); + + // Update the NSEC record. + nsec_update_bitmap( + key, + &record_nsec, + nsec, + &curr, + &add, + delete, + &set_nsec_rrsig, + iss, + ); + + // Mark descendents as occluded after updating the bitmap. + // The reason is that nsec_update_bitmap uses the current + // next_name and nsec_set_occluded may change that. + nsec_set_occluded(key, iss); + + continue; +*/ + } + if delete.contains(&Rtype::NS) { + // Apex is special, but we can assume the NS RRset will not + // be removed from apex. + assert!(*key != iss.origin); + + // Curr does not include all types at this name. Add the + // missing types to curr. + let range_key = (key.clone(), 0.into()); + let range = iss.new_data.range(range_key..); + for ((r_name, r_type), _) in range { + if r_name != key { + break; + } + if add.contains(r_type) { + // Skip what we are trying to add. + continue; + } + curr.insert(*r_type); + } + + let mut new = nsec3_update_bitmap( + key, + &record_nsec3, + nsec3, + &curr, + add, + delete, + iss, + ); + + // Sign the types at this name except for NSEC, and RRSIG. + new.remove(&Rtype::RRSIG); + sign_rtype_set(key, &new, iss)?; + + // Names that were previously occluded are no longer. + nsec3_clear_occluded(key, iss)?; + continue; + } + if *key != iss.origin && nsec3.types().contains(Rtype::NS) { + // NS marks a delegation but only when the NS is not + // at the apex. + + // If the add set contains DS then sign the DS RRset. + if add.contains(&Rtype::DS) { + let ds_set: HashSet<_> = [Rtype::DS].into(); + sign_rtype_set(key, &ds_set, iss)?; + } + nsec3_update_bitmap(key, &record_nsec3, nsec3, &curr, add, delete, iss); + continue; + + } + + // The add types need to be signed. + sign_rtype_set(key, add, iss)?; + + nsec3_update_bitmap(key, &record_nsec3, nsec3, &curr, add, delete, iss); + } else { +dbg!(key); +dbg!(add); +dbg!(delete); + if add.is_empty() { + assert!(!delete.is_empty()); + // No need to do anything. + continue; + } + assert!(delete.is_empty()); + if is_occluded(key, iss) { + // No need to do anything. + continue; + } + + if add.contains(&Rtype::NS) { + // Create a new NSEC record and sign only DS records (if any). + let rtypebitmap = nsec_rtypebitmap_from_iterator(add.iter()); + nsec_insert(key, rtypebitmap, iss); + if add.contains(&Rtype::DS) { + let ds_set: HashSet<_> = [Rtype::DS].into(); + sign_rtype_set(key, &ds_set, iss)?; + } + + // nsec_set_occluded expects the NSEC for key to exist. + // So call this after inserting the new NSEC record. + nsec_set_occluded(key, iss); + continue; + } + // The new name is not a delegation. Add RRSIG to the set of + // Rtypes. + let mut add_with_rrsig = add.clone(); + add_with_rrsig.insert(Rtype::RRSIG); + + // Create a new NSEC3 record and sign all records. + let rtypebitmap = nsec3_rtypebitmap_from_iterator(add_with_rrsig.iter()); + nsec3_insert_full(key, nsec3_hash_octets, &nsec3_name, rtypebitmap, iss); + sign_rtype_set(key, add, iss)?; + } + } + Ok(()) +} + fn nsec_insert( name: &Name, rtypebitmap: RtypeBitmap, @@ -2334,7 +2636,7 @@ fn nsec_update_bitmap( return curr; } - let rtypebitmap = rtypebitmap_from_iterator(curr.iter()); + let rtypebitmap = nsec_rtypebitmap_from_iterator(curr.iter()); let nsec = Nsec::new(nsec.next_name().clone(), rtypebitmap); let record = Record::new( record.owner().clone(), @@ -2444,7 +2746,7 @@ fn nsec_clear_occluded(name: &Name, iss: &mut IncrementalSigningState) -> } else { curr_types }; - let rtypebitmap = rtypebitmap_from_iterator(curr_types.iter()); + let rtypebitmap = nsec_rtypebitmap_from_iterator(curr_types.iter()); // Make sure NS doesn't get signed. curr_types.remove(&Rtype::NS); @@ -2454,33 +2756,429 @@ fn nsec_clear_occluded(name: &Name, iss: &mut IncrementalSigningState) -> Ok(()) } +fn nsec_rtypebitmap_from_iterator<'a, I>(iter: I) -> RtypeBitmap +where + I: Iterator, +{ + let mut rtypebitmap = RtypeBitmap::::builder(); + rtypebitmap.add(Rtype::NSEC).expect("should not fail"); + rtypebitmap.add(Rtype::RRSIG).expect("should not fail"); + for rtype in iter { + rtypebitmap.add(*rtype).expect("should not fail"); + } + rtypebitmap.finalize() +} + +fn nsec3_insert_full(name: &Name, nsec3_hash: OwnerHash, + nsec3_name: &Name, + rtypebitmap: RtypeBitmap, + iss: &mut IncrementalSigningState) { + println!("nsec3_insert_full: for name {name} nsec3 name {nsec3_name}"); + nsec3_insert_one(nsec3_hash, nsec3_name, rtypebitmap, iss); + + // Assume that we never insert the APEX. So the parent always exists. + let name = name.parent().expect("should exist"); + nsec3_insert_ent(&name, iss); +} + +fn nsec3_insert_ent(name: &Name, iss: &mut IncrementalSigningState) { + // Check if name has an NSEC3 record. If so, we are done. Otherwise, + // insert an ENT and continue with the parent. + println!("nsec3_insert_ent: for name {name}"); + let mut name = name.clone(); + loop { + if !name.ends_with(&iss.origin) { + // This is weird, we should never be able to get beyond APEX. + // Just ignore this. + return; + } + if name == iss.origin { + // APEX exists by definition. + return; + } + + let nsec3_hash_octets = + OwnerHash::::octets_from( + nsec3_hash::<_, _, BytesMut>(&name, + iss.nsec3param.hash_algorithm(), + iss.nsec3param.iterations(), + iss.nsec3param.salt()) + .expect("should not fail") + ); + let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); + let mut builder = NameBuilder::::new(); + builder.append_label(nsec3_hash_base32.as_bytes()).expect("should not fail"); + let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); + println!("nsec3_remove_et: got nsec3 name {nsec3_name} for name {name}"); + if iss.nsec3s.contains_key(&nsec3_name) { + // Found something. We are done. + return; + } + + let rtypebitmap = RtypeBitmap::::builder(); + let rtypebitmap = rtypebitmap.finalize(); + nsec3_insert_one(nsec3_hash_octets, &nsec3_name, rtypebitmap, iss); + + // Get the parent. We should be below APEX, so the parent has to exist. + name = name.parent().expect("parent should exist"); + } +} + +fn nsec3_insert_one( + nsec3_hash: OwnerHash, + nsec3_name: &Name, + rtypebitmap: RtypeBitmap, + iss: &mut IncrementalSigningState, +) { +println!("Inserting NSEC3 record {nsec3_name} with types {rtypebitmap}"); + // Try to find the NSEC3 record that comes before the one we are trying + // to insert. It is possible that we try to insert before the first NSEC3 + // record. In that case, logically try to insert after the last NSEC3 + // record. + let mut range = iss.nsec3s.range::, _>(..nsec3_name); + let (previous_name, previous_record) = + if let Some(kv) = range.next_back() { + kv + } else { + let mut range = iss.nsec3s.range::, _>(nsec3_name..); + range.next_back().expect("at least one element should exist") + }; +println!("nsec3_insert: found previous {previous_name} {previous_record:?}"); + let previous_name = previous_name.clone(); + let previous_record = previous_record.clone(); + drop(range); + let ZoneRecordData::Nsec3(previous_nsec3) = previous_record.data() else { + panic!("NSEC3 record expected"); + }; + let next = previous_nsec3.next_owner(); + let new_nsec3 = Nsec3::new(iss.nsec3param.hash_algorithm(), iss.nsec3param.flags(), iss.nsec3param.iterations(), iss.nsec3param.salt().clone(), next.clone(), rtypebitmap); + let new_record = Record::new( + nsec3_name.clone(), + previous_record.class(), + previous_record.ttl(), + ZoneRecordData::Nsec3(new_nsec3), + ); + iss.nsec3s.insert(nsec3_name.clone(), new_record); + iss.modified_nsecs.insert(nsec3_name.clone()); + let previous_nsec3 = Nsec3::new(iss.nsec3param.hash_algorithm(), iss.nsec3param.flags(), iss.nsec3param.iterations(), iss.nsec3param.salt().clone(), nsec3_hash, previous_nsec3.types().clone()); + let previous_record = Record::new( + previous_name.clone(), + previous_record.class(), + previous_record.ttl(), + ZoneRecordData::Nsec3(previous_nsec3), + ); + iss.nsec3s.insert(previous_name.clone(), previous_record); + iss.modified_nsecs.insert(previous_name.clone()); +} + +// Return the effective result HashSet even when the NSEC3 record gets deleted. +fn nsec3_update_bitmap( + name: &Name, + nsec3_record: &Zrd, + nsec3: &Nsec3, + curr: &HashSet, + add: &HashSet, + delete: &HashSet, + iss: &mut IncrementalSigningState, +) -> HashSet { + // Update curr. +println!("nsec3_update_bitmap: for name {name} record {nsec3_record:?} curr {curr:?} add {add:?} delete {delete:?}"); + let curr: HashSet<_> = curr.union(add).copied().collect(); + let mut curr: HashSet<_> = curr.difference(delete).copied().collect(); + let owner = nsec3_record.owner(); + + // Check if we need to add or remove RRSIG. Assume that apex has a SOA + // record. + if curr.contains(&Rtype::NS) && !curr.contains(&Rtype::SOA) { + // For an NS not at origin, there is an RRSIG if there is also a + // DS record. + if curr.contains(&Rtype::DS) { + // Yes, add RRSIG. + curr.insert(Rtype::RRSIG); + } else { + // Should remove RRSIG. + dbg!(curr); + todo!(); + } + } else { + // Is there anything apart from RRSIG? + if curr.iter().any(|r| *r != Rtype::RRSIG) { + // Yes. Add RRSIG. + curr.insert(Rtype::RRSIG); + } else { + // No. Remove RRSIG. + curr.remove(&Rtype::RRSIG); + } + } + + if curr.is_empty() { + nsec3_remove_full(name, owner, nsec3.next_owner(), iss); + return curr; + } + + let rtypebitmap = nsec3_rtypebitmap_from_iterator(curr.iter()); + let nsec3 = Nsec3::new(iss.nsec3param.hash_algorithm(), + iss.nsec3param.flags(), + iss.nsec3param.iterations(), + iss.nsec3param.salt().clone(), + nsec3.next_owner().clone(), rtypebitmap); + let record = Record::new( + nsec3_record.owner().clone(), + nsec3_record.class(), + nsec3_record.ttl(), + ZoneRecordData::Nsec3(nsec3), + ); + iss.nsec3s.insert(owner.clone(), record); + + iss.modified_nsecs.insert(owner.clone()); + curr +} + +fn nsec3_remove_full(name: &Name, nsec3_name: &Name, + nsec3_next: &OwnerHash, iss: &mut IncrementalSigningState) { + println!("nsec3_remove_full: for name {name} nsec3 name {nsec3_name}"); + nsec3_remove_one(nsec3_name, nsec3_next, iss); + + // Assume that we never remove the APEX. So the parent always exists. + let name = name.parent().expect("should exist"); + nsec3_remove_et(&name, iss); +} + +fn nsec3_remove_et(name: &Name, iss: &mut IncrementalSigningState) { + // Check if name is an ET. If so remove it and see if the parent is + // also an ET. + // + // Take a simple approach to check if a name is an ET: first lookup + // the NSEC3 record for name and check that the bitmap is empty. Then + // check all descendent names and check that none of them has an + // NSEC3 record. + println!("nsec3_remove_et: for name {name}"); + let mut name = name.clone(); + loop { + if !name.ends_with(&iss.origin) { + // This is weird, we should never be able to get beyond APEX. + // Just ignore this. + return; + } + if name == iss.origin { + // Never remove the NSEC3 record for APEX. + return; + } + + let nsec3_hash_octets = + OwnerHash::::octets_from( + nsec3_hash::<_, _, BytesMut>(&name, + iss.nsec3param.hash_algorithm(), + iss.nsec3param.iterations(), + iss.nsec3param.salt()) + .expect("should not fail") + ); + let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); + let mut builder = NameBuilder::::new(); + builder.append_label(nsec3_hash_base32.as_bytes()).expect("should not fail"); + let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); + println!("nsec3_remove_et: got nsec3 name {nsec3_name} for name {name}"); + let Some(record_nsec3) = iss.nsec3s.get(&nsec3_name) else { + // No NSEC3 record, nothing to do. + return; + }; + + let ZoneRecordData::Nsec3(nsec3) = record_nsec3.data() else { + panic!("NSEC3 record expected"); + }; + + if !nsec3.types().is_empty() { + // There are types here. + return; + } + + // Check the descendents. + let key = (name.clone(), Rtype::SOA); + let range = iss.new_data.range(key..); + let mut opt_curr_name: Option<&Name> = None; + + for ((key_name, _), _) in range { + println!("trying {key_name} for {name}"); + // There is no easy way to avoid name showing up in the range. Just + // filter out name. + if *key_name == name { + continue; + } + + // Make sure curr_name is below name. + if !key_name.ends_with(&name) { + break; + } + + todo!(); + } + + // No descendents with NSEC3 records are found. Delete this one. + let next_owner = nsec3.next_owner().clone(); + nsec3_remove_one(&nsec3_name, &next_owner, iss); + + // We remove the NSEC3 record for the name. Get the parent. We should + // be below APEX, so the parent has to exist. + name = name.parent().expect("parent should exist"); + } +} + +fn nsec3_remove_one(nsec3_name: &Name, nsec3_next: &OwnerHash, iss: &mut IncrementalSigningState) { + println!("nsec3_remove_one: for {nsec3_name}"); + + // Try to find the NSEC3 record that comes before the one we are trying + // to remove. + let mut range = iss.nsec3s.range::, _>(..nsec3_name); + let (previous_name, previous_record) = + if let Some(kv) = range.next_back() { + kv + } else { + let mut range = iss.nsec3s.range::, _>(nsec3_name..); + range.next_back().expect("at least one element should exist") + }; + + let previous_name = previous_name.clone(); + let previous_record = previous_record.clone(); + drop(range); + let ZoneRecordData::Nsec3(previous_nsec) = previous_record.data() else { + panic!("NSEC3 record expected"); + }; + let previous_nsec3 = Nsec3::new(iss.nsec3param.hash_algorithm(), iss.nsec3param.flags(), iss.nsec3param.iterations(), iss.nsec3param.salt().clone(), nsec3_next.clone(), previous_nsec.types().clone()); + let previous_record = Record::new( + previous_name.clone(), + previous_record.class(), + previous_record.ttl(), + ZoneRecordData::Nsec3(previous_nsec3), + ); + iss.nsec3s.insert(previous_name.clone(), previous_record); + iss.modified_nsecs.insert(previous_name.clone()); + iss.nsec3s.remove(nsec3_name); + iss.modified_nsecs.remove(nsec3_name); + let key = (nsec3_name.clone(), Rtype::NSEC3); + iss.rrsigs.remove(&key); +} + +fn nsec3_clear_occluded(name: &Name, iss: &mut IncrementalSigningState) -> Result<(), Error> { + println!("nsec3_clear_occluded: for {name}"); + let key = (name.clone(), Rtype::SOA); + let range = iss.new_data.range(key..); + let mut opt_curr_name: Option<&Name> = None; + let mut curr_types: HashSet = HashSet::new(); + let mut work = vec![]; + + // Keep track of delegations. Name below a delegation remain occluded. + let mut delegation: Option> = None; + + for ((key_name, key_rtype), _) in range { + // There is no easy way to avoid name showing up in the range. Just + // filter out name. + if key_name == name { + continue; + } + + // Make sure curr_name is below name. + if !key_name.ends_with(name) { + break; + } + if let Some(d) = &delegation { + if key_name.ends_with(d) && key_name != d { + // Skip. + continue; + } + } + if *key_rtype == Rtype::NS { + // Set key_name as a delegation. + delegation = Some(key_name.clone()); + } + if let Some(curr_name) = opt_curr_name { + if key_name == curr_name { + curr_types.insert(*key_rtype); + } else { + work.push((curr_name.clone(), curr_types)); + opt_curr_name = Some(key_name); + curr_types = [*key_rtype].into(); + } + } else { + opt_curr_name = Some(key_name); + curr_types.insert(*key_rtype); + } + } + if let Some(curr_name) = opt_curr_name { + work.push((curr_name.clone(), curr_types)); + } + for (curr_name, curr_types) in work { + let mut curr_types = if curr_types.contains(&Rtype::NS) { + let has_ds = curr_types.contains(&Rtype::DS); + let mut curr_types: HashSet = [Rtype::NS].into(); + if has_ds { + curr_types.insert(Rtype::DS); + } + curr_types + } else { + curr_types + }; + let rtypebitmap = nsec3_rtypebitmap_from_iterator(curr_types.iter()); + + // Make sure NS doesn't get signed. + curr_types.remove(&Rtype::NS); + sign_rtype_set(&curr_name, &curr_types, iss)?; + + let nsec3_hash_octets = + OwnerHash::::octets_from( + nsec3_hash::<_, _, BytesMut>(&curr_name, + iss.nsec3param.hash_algorithm(), + iss.nsec3param.iterations(), + iss.nsec3param.salt()) + .expect("should not fail") + ); + let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); + let mut builder = NameBuilder::::new(); + builder.append_label(nsec3_hash_base32.as_bytes()).expect("should not fail"); + let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); + nsec3_insert_full(&curr_name, nsec3_hash_octets, &nsec3_name, rtypebitmap, iss); + } + Ok(()) +} + + +fn nsec3_rtypebitmap_from_iterator<'a, I>(iter: I) -> RtypeBitmap +where + I: Iterator, +{ + let mut rtypebitmap = RtypeBitmap::::builder(); + for rtype in iter { + rtypebitmap.add(*rtype).expect("should not fail"); + } + rtypebitmap.finalize() +} + fn is_occluded(name: &Name, iss: &IncrementalSigningState) -> bool { // We need to check if the parent of name is a delegation. Stop // when we reached origin. let Some(mut curr) = name.parent() else { - // We asked for the parent of the root. That is weird. Just - // return no occluded. - return false; + // We asked for the parent of the root. That is weird. Just + // return not occluded. + return false; }; loop { - if curr == iss.origin { - // We reached apex. The name was not occluded. - return false; - } - if !curr.ends_with(&iss.origin) { - // Something weird is going on. Return not occluded. - return false; - } - if iss.new_data.contains_key(&(curr.clone(), Rtype::NS)) { - // Name is occluded. - return true; - } - let Some(parent) = curr.parent() else { - // We asked for the parent of the root. That is weird. Just - // return no occluded. - return false; - }; - curr = parent; + if curr == iss.origin { + // We reached apex. The name was not occluded. + return false; + } + if !curr.ends_with(&iss.origin) { + // Something weird is going on. Return not occluded. + return false; + } + if iss.new_data.contains_key(&(curr.clone(), Rtype::NS)) { + // Name is occluded. + return true; + } + let Some(parent) = curr.parent() else { + // We asked for the parent of the root. That is weird. Just + // return not occluded. + return false; + }; + curr = parent; } } @@ -2489,23 +3187,25 @@ fn sign_rtype_set( set: &HashSet, iss: &mut IncrementalSigningState, ) -> Result<(), Error> { + println!("sign_rtype_set: name {name} set {set:?}"); let mut new_sigs = vec![]; for rtype in set { - let key = (name.clone(), *rtype); - let Some(records) = iss.new_data.get(&key) else { - panic!("Expected something for {name}/{rtype}"); - }; - sign_records( - records, - &iss.keys, - iss.inception, - iss.expiration, - &mut new_sigs, - )?; + let key = (name.clone(), *rtype); + let Some(records) = iss.new_data.get(&key) else { + panic!("Expected something for {name}/{rtype}"); + }; + sign_records( + records, + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; } for (sig, rtype) in new_sigs { - let key = (sig[0].owner().clone(), rtype); - iss.rrsigs.insert(key, sig); + let key = (sig[0].owner().clone(), rtype); + println!("sign_rtype_set: rrsig.insert {key:?} {sig:?}"); + iss.rrsigs.insert(key, sig); } Ok(()) } @@ -2519,41 +3219,28 @@ fn sign_records( ) -> Result<(), Error> { let rtype = records[0].rtype(); if rtype == Rtype::DNSKEY || rtype == Rtype::CDS || rtype == Rtype::CDNSKEY { - // These records get signed with the KSK(s). Don't touch - // the signatures. - return Ok(()); + // These records get signed with the KSK(s). Don't touch + // the signatures. + return Ok(()); } let rrset = Rrset::new(records).map_err(|e| format!("Rrset::new failed: {e}"))?; let mut rrsig_records = vec![]; for key in keys { - let rrsig = sign_rrset(key, &rrset, inception, expiration) - .map_err(|e| format!("signing failed: {e}"))?; - let record = Record::new( - rrsig.owner().clone(), - rrsig.class(), - rrsig.ttl(), - ZoneRecordData::Rrsig(rrsig.data().clone()), - ); - rrsig_records.push(record); + let rrsig = sign_rrset(key, &rrset, inception, expiration) + .map_err(|e| format!("signing failed: {e}"))?; + let record = Record::new( + rrsig.owner().clone(), + rrsig.class(), + rrsig.ttl(), + ZoneRecordData::Rrsig(rrsig.data().clone()), + ); + rrsig_records.push(record); } new_sigs.push((rrsig_records, rrset.rtype())); Ok(()) } -fn rtypebitmap_from_iterator<'a, I>(iter: I) -> RtypeBitmap -where - I: Iterator, -{ - let mut rtypebitmap = RtypeBitmap::::builder(); - rtypebitmap.add(Rtype::NSEC).expect("should not fail"); - rtypebitmap.add(Rtype::RRSIG).expect("should not fail"); - for rtype in iter { - rtypebitmap.add(*rtype).expect("should not fail"); - } - rtypebitmap.finalize() -} - //------------ SigningMode --------------------------------------------------- #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] From b09ac295145fde306ea9f1514dffd6363a6bef7a Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Tue, 3 Feb 2026 17:30:17 +0100 Subject: [PATCH 08/20] All 3 tests pass for NSEC3. --- src/commands/signer.rs | 927 +++++++++++++++++++++++------------------ 1 file changed, 518 insertions(+), 409 deletions(-) diff --git a/src/commands/signer.rs b/src/commands/signer.rs index c1f124d8..eda9f825 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -3,16 +3,13 @@ use super::nsec3hash::Nsec3Hash; use crate::env::{Env, Stream}; use crate::error::{Context, Error}; use crate::DISPLAY_KIND; -use bytes::{BytesMut, BufMut, Bytes}; +use bytes::{BufMut, Bytes, BytesMut}; use clap::builder::ValueParser; use clap::Subcommand; use core::clone::Clone; use core::cmp::Ordering; use core::fmt::Write; use core::str::FromStr; -use domain::rdata::nsec3::OwnerHash; -use domain::utils::base32; -use domain::dnssec::common::nsec3_hash; use domain::base::iana::nsec3::Nsec3HashAlgorithm; use domain::base::iana::zonemd::{ZonemdAlgorithm, ZonemdScheme}; use domain::base::iana::Class; @@ -23,7 +20,7 @@ use domain::base::{ }; use domain::crypto::sign::{KeyPair, SecretKeyBytes}; use domain::dep::octseq::OctetsFrom; -use domain::dnssec::common::parse_from_bind; +use domain::dnssec::common::{nsec3_hash, parse_from_bind}; use domain::dnssec::sign::denial::config::DenialConfig; use domain::dnssec::sign::denial::nsec::GenerateNsecConfig; use domain::dnssec::sign::denial::nsec3::{mk_hashed_nsec3_owner_name, GenerateNsec3Config}; @@ -36,9 +33,9 @@ use domain::dnssec::sign::traits::{Signable, SignableZoneInPlace}; use domain::dnssec::sign::SigningConfig; use domain::dnssec::validator::base::DnskeyExt; use domain::rdata::dnssec::{RtypeBitmap, Timestamp}; -use domain::rdata::nsec3::Nsec3Salt; +use domain::rdata::nsec3::{Nsec3Salt, OwnerHash}; use domain::rdata::{Dnskey, Nsec, Nsec3, Nsec3param, Rrsig, Soa, ZoneRecordData, Zonemd}; -use domain::utils::base64; +use domain::utils::{base32, base64}; use domain::zonefile::inplace::{self, Entry}; use domain::zonetree::types::StoredRecordData; use domain::zonetree::{StoredName, StoredRecord}; @@ -49,7 +46,6 @@ use octseq::builder::with_infallible; use rayon::slice::ParallelSliceMut; use ring::digest; use serde::{Deserialize, Serialize}; -use std::time::Instant; use std::cmp::min; use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt::{self, Display}; @@ -58,7 +54,7 @@ use std::io::Write as IoWrite; use std::io::{self, /*stdout,*/ BufWriter}; use std::path::{Path, PathBuf}; use std::process::Command; -use std::time::UNIX_EPOCH; +use std::time::{Instant, UNIX_EPOCH}; use tokio::time::Duration; use tracing::warn; use url::Url; @@ -1227,120 +1223,117 @@ impl Signer { iss.keys = keys; - let start = Instant::now(); + let start = Instant::now(); load_signed_zone(&mut iss, &sc.zonefile_out).unwrap(); - println!("loading signed zone took {:?}", start.elapsed()); - - // Note that we could try to regenerate the NSEC(3). Assume that - // switching between NSEC, NSEC3, and NSEC3 opt-out (or other NSEC3 - // parameter changes) is rare enough that we can just resign the full - // zone. - let key = (iss.origin.clone(), Rtype::NSEC3PARAM); - let opt_nsec3param = iss.old_data.get(&key); - if let Some(nsec3param_records) = opt_nsec3param { - // Zone was signed with NSEC3. - if !sc.use_nsec3 { - // Zone is signed with NSEC3 but we want NSEC. - todo!(); - } - let ZoneRecordData::Nsec3param(nsec3param) = nsec3param_records[0].data() else { - panic!("ZoneRecordData::Nsec3param expected"); - }; - if nsec3param.hash_algorithm() != sc.algorithm || - nsec3param.opt_out_flag() != sc.opt_out || - nsec3param.iterations() != sc.iterations || - *nsec3param.salt() != sc.salt { - // Parameters changed, resign. - todo!(); - } - - // Nothing has changed. Insert the old NSEC3PARAM records in the - // new zone data. - iss.new_data.insert(key, nsec3param_records.to_vec()); - } else { - // Zone was signed with NSEC, check if that is also the target. - if sc.use_nsec3 { - // Resign the full zone with NSEC3. - todo!(); - } - - // Stay with NSEC. - } - - let start = Instant::now(); + println!("loading signed zone took {:?}", start.elapsed()); + + // Note that we could try to regenerate the NSEC(3). Assume that + // switching between NSEC, NSEC3, and NSEC3 opt-out (or other NSEC3 + // parameter changes) is rare enough that we can just resign the full + // zone. + let key = (iss.origin.clone(), Rtype::NSEC3PARAM); + let opt_nsec3param = iss.old_data.get(&key); + if let Some(nsec3param_records) = opt_nsec3param { + // Zone was signed with NSEC3. + if !sc.use_nsec3 { + // Zone is signed with NSEC3 but we want NSEC. + todo!(); + } + let ZoneRecordData::Nsec3param(nsec3param) = nsec3param_records[0].data() else { + panic!("ZoneRecordData::Nsec3param expected"); + }; + if nsec3param.hash_algorithm() != sc.algorithm + || nsec3param.opt_out_flag() != sc.opt_out + || nsec3param.iterations() != sc.iterations + || *nsec3param.salt() != sc.salt + { + // Parameters changed, resign. + todo!(); + } + + // Nothing has changed. Insert the old NSEC3PARAM records in the + // new zone data. + iss.new_data.insert(key, nsec3param_records.to_vec()); + } else { + // Zone was signed with NSEC, check if that is also the target. + if sc.use_nsec3 { + // Resign the full zone with NSEC3. + todo!(); + } + + // Stay with NSEC. + } + + let start = Instant::now(); load_unsigned_zone(&mut iss, &sc.zonefile_in).unwrap(); - println!("loading new unsigned zone took {:?}", start.elapsed()); + println!("loading new unsigned zone took {:?}", start.elapsed()); - let start = Instant::now(); + let start = Instant::now(); load_apex_records(kss, &mut iss)?; initial_diffs(&mut iss)?; - if sc.use_nsec3 { - if sc.opt_out { - todo!(); - } else { - incremental_nsec3(&mut iss)?; - } - } - else { - incremental_nsec(&mut iss)?; - } + if sc.use_nsec3 { + if sc.opt_out { + todo!(); + } else { + incremental_nsec3(&mut iss)?; + } + } else { + incremental_nsec(&mut iss)?; + } let mut new_sigs = vec![]; - if sc.use_nsec3 { - for m in &iss.modified_nsecs { - let Some(nsec3) = iss.nsec3s.get(m) else { - panic!("NSEC3 for {m} should exist"); - }; - - let nsec3 = nsec3.clone(); - sign_records( - &[nsec3], - &iss.keys, - iss.inception, - iss.expiration, - &mut new_sigs, - )?; - } - } else { - for m in &iss.modified_nsecs { - let Some(nsec) = iss.nsecs.get(m) else { - panic!("NSEC for {m} should exist"); - }; - - let nsec = nsec.clone(); - sign_records( - &[nsec], - &iss.keys, - iss.inception, - iss.expiration, - &mut new_sigs, - )?; - } - } + if sc.use_nsec3 { + for m in &iss.modified_nsecs { + let Some(nsec3) = iss.nsec3s.get(m) else { + panic!("NSEC3 for {m} should exist"); + }; + + let nsec3 = nsec3.clone(); + sign_records( + &[nsec3], + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + } + } else { + for m in &iss.modified_nsecs { + let Some(nsec) = iss.nsecs.get(m) else { + panic!("NSEC for {m} should exist"); + }; + + let nsec = nsec.clone(); + sign_records( + &[nsec], + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + } + } for (sig, rtype) in new_sigs { let key = (sig[0].owner().clone(), rtype); iss.rrsigs.insert(key, sig); } - println!("incremental signing took {:?}", start.elapsed()); + println!("incremental signing took {:?}", start.elapsed()); -/* - let mut writer = stdout(); -*/ + /* + let mut writer = stdout(); + */ - let start = Instant::now(); - let mut writer = { - let filename = sc.zonefile_out.as_os_str().to_str().unwrap(); - let filename = filename.to_owned() + "-incremental"; - let file = File::create(&filename).map_err(|e| { - format!( - "unable to create file {filename}: {e}", - ) - })?; + let start = Instant::now(); + let mut writer = { + let filename = sc.zonefile_out.as_os_str().to_str().unwrap(); + let filename = filename.to_owned() + "-incremental"; + let file = File::create(&filename) + .map_err(|e| format!("unable to create file {filename}: {e}",))?; BufWriter::new(file) // FileOrStdout::File(file) - }; + }; for (_, data) in iss.new_data { for rr in data { @@ -1370,7 +1363,7 @@ impl Signer { .map_err(|e| format!("unable write signed zone: {e}"))?; } } - println!("writing output took {:?}", start.elapsed()); + println!("writing output took {:?}", start.elapsed()); todo!(); } @@ -1845,13 +1838,13 @@ impl IncrementalSigningState { let inception = (now - sc.inception_offset.as_secs() as u32).into(); let expiration = (now + sc.signature_lifetime.as_secs() as u32).into(); - // This is the only way to deal with opt-out. There is no data type - // for flags or constant for opt-out. Creating an Nsec3param makes it - // possible to set opt-out. - let mut nsec3param = Nsec3param::new(sc.algorithm, 0, sc.iterations, sc.salt.clone()); - if sc.opt_out { - nsec3param.set_opt_out_flag(); - } + // This is the only way to deal with opt-out. There is no data type + // for flags or constant for opt-out. Creating an Nsec3param makes it + // possible to set opt-out. + let mut nsec3param = Nsec3param::new(sc.algorithm, 0, sc.iterations, sc.salt.clone()); + if sc.opt_out { + nsec3param.set_opt_out_flag(); + } Self { origin, old_data: HashMap::new(), @@ -1864,7 +1857,7 @@ impl IncrementalSigningState { keys: vec![], inception, expiration, - nsec3param, + nsec3param, } } } @@ -1934,7 +1927,7 @@ fn load_signed_zone(iss: &mut IncrementalSigningState, path: &PathBuf) -> Result // Assume (at most) one NSEC3 record per owner name. // Directly insert into the btree map. iss.nsec3s.insert(record.owner().clone(), record); - } + } _ => { if records.is_empty() { records.push(record); @@ -2141,13 +2134,12 @@ fn initial_diffs(iss: &mut IncrementalSigningState) -> Result<(), Error> { for (_, new_rrset) in iss.new_data.iter_mut() { let key = (new_rrset[0].owner().clone(), new_rrset[0].rtype()); if let Some(mut old_rrset) = iss.old_data.remove(&key) { - let rtype = new_rrset[0].rtype(); - if rtype == Rtype::DNSKEY || rtype == Rtype::CDS || - rtype == Rtype::CDNSKEY { - // These types are signed by the key manager. No need to - // check for changes. - continue; - } + let rtype = new_rrset[0].rtype(); + if rtype == Rtype::DNSKEY || rtype == Rtype::CDS || rtype == Rtype::CDNSKEY { + // These types are signed by the key manager. No need to + // check for changes. + continue; + } old_rrset.sort_by(|a, b| a.as_ref().data().canonical_cmp(b.as_ref().data())); new_rrset.sort_by(|a, b| a.as_ref().data().canonical_cmp(b.as_ref().data())); @@ -2235,7 +2227,7 @@ fn incremental_nsec(iss: &mut IncrementalSigningState) -> Result<(), Error> { for rtype in nsec.types().iter() { // When NS is added, we should keep the signatures for // DS and NSEC. The NSEC signature will be updated but - // there is no point in removing it first. Do no try to + // there is no point in removing it first. Do not try to // remove a signature for RRSIG because it does not exist. if rtype == Rtype::DS || rtype == Rtype::NSEC || rtype == Rtype::RRSIG { continue; @@ -2372,18 +2364,21 @@ fn incremental_nsec3(iss: &mut IncrementalSigningState) -> Result<(), Error> { // The intersection between add and delete is empty. assert!(add.intersection(delete).next().is_none()); - let nsec3_hash_octets = - OwnerHash::::octets_from( - nsec3_hash::<_, _, BytesMut>(key, - iss.nsec3param.hash_algorithm(), - iss.nsec3param.iterations(), - iss.nsec3param.salt()) - .expect("should not fail") - ); - let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); - let mut builder = NameBuilder::::new(); - builder.append_label(nsec3_hash_base32.as_bytes()).expect("should not fail"); - let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); + let nsec3_hash_octets = OwnerHash::::octets_from( + nsec3_hash::<_, _, BytesMut>( + key, + iss.nsec3param.hash_algorithm(), + iss.nsec3param.iterations(), + iss.nsec3param.salt(), + ) + .expect("should not fail"), + ); + let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); + let mut builder = NameBuilder::::new(); + builder + .append_label(nsec3_hash_base32.as_bytes()) + .expect("should not fail"); + let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); if let Some(record_nsec3) = iss.nsec3s.get(&nsec3_name) { let record_nsec3 = record_nsec3.clone(); let ZoneRecordData::Nsec3(nsec3) = record_nsec3.data() else { @@ -2404,19 +2399,16 @@ fn incremental_nsec3(iss: &mut IncrementalSigningState) -> Result<(), Error> { assert!(delete.difference(&curr).next().is_none()); if add.contains(&Rtype::NS) { - todo!(); -/* // Apex is special, but we can assume the NS RRset will not // be added to apex. assert!(*key != iss.origin); // Remove the signatures for the existing types. - for rtype in nsec.types().iter() { + for rtype in nsec3.types().iter() { // When NS is added, we should keep the signatures for - // DS and NSEC. The NSEC signature will be updated but - // there is no point in removing it first. Do no try to - // remove a signature for RRSIG because it does not exist. - if rtype == Rtype::DS || rtype == Rtype::NSEC || rtype == Rtype::RRSIG { + // DS. Do not try to remove a signature for RRSIG because + // it does not exist. + if rtype == Rtype::DS || rtype == Rtype::RRSIG { continue; } let key = (key.clone(), rtype); @@ -2424,30 +2416,20 @@ fn incremental_nsec3(iss: &mut IncrementalSigningState) -> Result<(), Error> { } // Restrict curr and add to these types. - let mask: HashSet = [Rtype::NS, Rtype::DS, Rtype::NSEC, Rtype::RRSIG].into(); + let mask: HashSet = [Rtype::NS, Rtype::DS, Rtype::RRSIG].into(); let curr = curr.intersection(&mask).copied().collect(); let add = add.intersection(&mask).copied().collect(); - // Update the NSEC record. - nsec_update_bitmap( - key, - &record_nsec, - nsec, - &curr, - &add, - delete, - &set_nsec_rrsig, - iss, - ); + // Update the NSEC3 record. + nsec3_update_bitmap(key, &record_nsec3, nsec3, &curr, &add, delete, iss); // Mark descendents as occluded after updating the bitmap. - // The reason is that nsec_update_bitmap uses the current - // next_name and nsec_set_occluded may change that. - nsec_set_occluded(key, iss); + // The reason is that nsec3_update_bitmap uses the current + // next_hash and nsec3_set_occluded may change that. + nsec3_set_occluded(key, iss); continue; -*/ } if delete.contains(&Rtype::NS) { // Apex is special, but we can assume the NS RRset will not @@ -2469,15 +2451,8 @@ fn incremental_nsec3(iss: &mut IncrementalSigningState) -> Result<(), Error> { curr.insert(*r_type); } - let mut new = nsec3_update_bitmap( - key, - &record_nsec3, - nsec3, - &curr, - add, - delete, - iss, - ); + let mut new = + nsec3_update_bitmap(key, &record_nsec3, nsec3, &curr, add, delete, iss); // Sign the types at this name except for NSEC, and RRSIG. new.remove(&Rtype::RRSIG); @@ -2498,7 +2473,6 @@ fn incremental_nsec3(iss: &mut IncrementalSigningState) -> Result<(), Error> { } nsec3_update_bitmap(key, &record_nsec3, nsec3, &curr, add, delete, iss); continue; - } // The add types need to be signed. @@ -2506,9 +2480,6 @@ fn incremental_nsec3(iss: &mut IncrementalSigningState) -> Result<(), Error> { nsec3_update_bitmap(key, &record_nsec3, nsec3, &curr, add, delete, iss); } else { -dbg!(key); -dbg!(add); -dbg!(delete); if add.is_empty() { assert!(!delete.is_empty()); // No need to do anything. @@ -2521,23 +2492,26 @@ dbg!(delete); } if add.contains(&Rtype::NS) { - // Create a new NSEC record and sign only DS records (if any). - let rtypebitmap = nsec_rtypebitmap_from_iterator(add.iter()); - nsec_insert(key, rtypebitmap, iss); + // Create a new NSEC3 record and sign only DS records (if any). + // If add contains DS then add RRSIG to add. + + let mut add = add.clone(); // In case we need to add RRSIG. if add.contains(&Rtype::DS) { let ds_set: HashSet<_> = [Rtype::DS].into(); sign_rtype_set(key, &ds_set, iss)?; + add.insert(Rtype::RRSIG); } - // nsec_set_occluded expects the NSEC for key to exist. - // So call this after inserting the new NSEC record. - nsec_set_occluded(key, iss); + let rtypebitmap = nsec3_rtypebitmap_from_iterator(add.iter()); + + nsec3_insert_full(key, nsec3_hash_octets, &nsec3_name, rtypebitmap, iss); + nsec3_set_occluded(key, iss); continue; } - // The new name is not a delegation. Add RRSIG to the set of - // Rtypes. - let mut add_with_rrsig = add.clone(); - add_with_rrsig.insert(Rtype::RRSIG); + // The new name is not a delegation. Add RRSIG to the set of + // Rtypes. + let mut add_with_rrsig = add.clone(); + add_with_rrsig.insert(Rtype::RRSIG); // Create a new NSEC3 record and sign all records. let rtypebitmap = nsec3_rtypebitmap_from_iterator(add_with_rrsig.iter()); @@ -2769,11 +2743,13 @@ where rtypebitmap.finalize() } -fn nsec3_insert_full(name: &Name, nsec3_hash: OwnerHash, +fn nsec3_insert_full( + name: &Name, + nsec3_hash: OwnerHash, nsec3_name: &Name, rtypebitmap: RtypeBitmap, - iss: &mut IncrementalSigningState) { - println!("nsec3_insert_full: for name {name} nsec3 name {nsec3_name}"); + iss: &mut IncrementalSigningState, +) { nsec3_insert_one(nsec3_hash, nsec3_name, rtypebitmap, iss); // Assume that we never insert the APEX. So the parent always exists. @@ -2784,43 +2760,44 @@ fn nsec3_insert_full(name: &Name, nsec3_hash: OwnerHash, fn nsec3_insert_ent(name: &Name, iss: &mut IncrementalSigningState) { // Check if name has an NSEC3 record. If so, we are done. Otherwise, // insert an ENT and continue with the parent. - println!("nsec3_insert_ent: for name {name}"); let mut name = name.clone(); loop { - if !name.ends_with(&iss.origin) { - // This is weird, we should never be able to get beyond APEX. - // Just ignore this. - return; - } - if name == iss.origin { - // APEX exists by definition. - return; - } - - let nsec3_hash_octets = - OwnerHash::::octets_from( - nsec3_hash::<_, _, BytesMut>(&name, - iss.nsec3param.hash_algorithm(), - iss.nsec3param.iterations(), - iss.nsec3param.salt()) - .expect("should not fail") - ); - let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); - let mut builder = NameBuilder::::new(); - builder.append_label(nsec3_hash_base32.as_bytes()).expect("should not fail"); - let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); - println!("nsec3_remove_et: got nsec3 name {nsec3_name} for name {name}"); + if !name.ends_with(&iss.origin) { + // This is weird, we should never be able to get beyond APEX. + // Just ignore this. + return; + } + if name == iss.origin { + // APEX exists by definition. + return; + } + + let nsec3_hash_octets = OwnerHash::::octets_from( + nsec3_hash::<_, _, BytesMut>( + &name, + iss.nsec3param.hash_algorithm(), + iss.nsec3param.iterations(), + iss.nsec3param.salt(), + ) + .expect("should not fail"), + ); + let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); + let mut builder = NameBuilder::::new(); + builder + .append_label(nsec3_hash_base32.as_bytes()) + .expect("should not fail"); + let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); if iss.nsec3s.contains_key(&nsec3_name) { - // Found something. We are done. - return; - } - - let rtypebitmap = RtypeBitmap::::builder(); - let rtypebitmap = rtypebitmap.finalize(); - nsec3_insert_one(nsec3_hash_octets, &nsec3_name, rtypebitmap, iss); + // Found something. We are done. + return; + } + + let rtypebitmap = RtypeBitmap::::builder(); + let rtypebitmap = rtypebitmap.finalize(); + nsec3_insert_one(nsec3_hash_octets, &nsec3_name, rtypebitmap, iss); - // Get the parent. We should be below APEX, so the parent has to exist. - name = name.parent().expect("parent should exist"); + // Get the parent. We should be below APEX, so the parent has to exist. + name = name.parent().expect("parent should exist"); } } @@ -2830,20 +2807,19 @@ fn nsec3_insert_one( rtypebitmap: RtypeBitmap, iss: &mut IncrementalSigningState, ) { -println!("Inserting NSEC3 record {nsec3_name} with types {rtypebitmap}"); // Try to find the NSEC3 record that comes before the one we are trying // to insert. It is possible that we try to insert before the first NSEC3 // record. In that case, logically try to insert after the last NSEC3 // record. let mut range = iss.nsec3s.range::, _>(..nsec3_name); - let (previous_name, previous_record) = - if let Some(kv) = range.next_back() { - kv - } else { - let mut range = iss.nsec3s.range::, _>(nsec3_name..); - range.next_back().expect("at least one element should exist") - }; -println!("nsec3_insert: found previous {previous_name} {previous_record:?}"); + let (previous_name, previous_record) = if let Some(kv) = range.next_back() { + kv + } else { + let mut range = iss.nsec3s.range::, _>(nsec3_name..); + range + .next_back() + .expect("at least one element should exist") + }; let previous_name = previous_name.clone(); let previous_record = previous_record.clone(); drop(range); @@ -2851,7 +2827,14 @@ println!("nsec3_insert: found previous {previous_name} {previous_record:?}"); panic!("NSEC3 record expected"); }; let next = previous_nsec3.next_owner(); - let new_nsec3 = Nsec3::new(iss.nsec3param.hash_algorithm(), iss.nsec3param.flags(), iss.nsec3param.iterations(), iss.nsec3param.salt().clone(), next.clone(), rtypebitmap); + let new_nsec3 = Nsec3::new( + iss.nsec3param.hash_algorithm(), + iss.nsec3param.flags(), + iss.nsec3param.iterations(), + iss.nsec3param.salt().clone(), + next.clone(), + rtypebitmap, + ); let new_record = Record::new( nsec3_name.clone(), previous_record.class(), @@ -2860,7 +2843,14 @@ println!("nsec3_insert: found previous {previous_name} {previous_record:?}"); ); iss.nsec3s.insert(nsec3_name.clone(), new_record); iss.modified_nsecs.insert(nsec3_name.clone()); - let previous_nsec3 = Nsec3::new(iss.nsec3param.hash_algorithm(), iss.nsec3param.flags(), iss.nsec3param.iterations(), iss.nsec3param.salt().clone(), nsec3_hash, previous_nsec3.types().clone()); + let previous_nsec3 = Nsec3::new( + iss.nsec3param.hash_algorithm(), + iss.nsec3param.flags(), + iss.nsec3param.iterations(), + iss.nsec3param.salt().clone(), + nsec3_hash, + previous_nsec3.types().clone(), + ); let previous_record = Record::new( previous_name.clone(), previous_record.class(), @@ -2882,7 +2872,6 @@ fn nsec3_update_bitmap( iss: &mut IncrementalSigningState, ) -> HashSet { // Update curr. -println!("nsec3_update_bitmap: for name {name} record {nsec3_record:?} curr {curr:?} add {add:?} delete {delete:?}"); let curr: HashSet<_> = curr.union(add).copied().collect(); let mut curr: HashSet<_> = curr.difference(delete).copied().collect(); let owner = nsec3_record.owner(); @@ -2890,43 +2879,45 @@ println!("nsec3_update_bitmap: for name {name} record {nsec3_record:?} curr {cur // Check if we need to add or remove RRSIG. Assume that apex has a SOA // record. if curr.contains(&Rtype::NS) && !curr.contains(&Rtype::SOA) { - // For an NS not at origin, there is an RRSIG if there is also a - // DS record. - if curr.contains(&Rtype::DS) { - // Yes, add RRSIG. - curr.insert(Rtype::RRSIG); - } else { - // Should remove RRSIG. - dbg!(curr); - todo!(); - } + // For an NS not at origin, there is an RRSIG if there is also a + // DS record. + if curr.contains(&Rtype::DS) { + // Yes, add RRSIG. + curr.insert(Rtype::RRSIG); + } else { + // No. Remove RRSIG. + curr.remove(&Rtype::RRSIG); + } } else { - // Is there anything apart from RRSIG? - if curr.iter().any(|r| *r != Rtype::RRSIG) { - // Yes. Add RRSIG. - curr.insert(Rtype::RRSIG); - } else { - // No. Remove RRSIG. - curr.remove(&Rtype::RRSIG); - } - } - + // Is there anything apart from RRSIG? + if curr.iter().any(|r| *r != Rtype::RRSIG) { + // Yes. Add RRSIG. + curr.insert(Rtype::RRSIG); + } else { + // No. Remove RRSIG. + curr.remove(&Rtype::RRSIG); + } + } + if curr.is_empty() { - nsec3_remove_full(name, owner, nsec3.next_owner(), iss); - return curr; + nsec3_remove_full(name, owner, nsec3.next_owner(), iss); + return curr; } let rtypebitmap = nsec3_rtypebitmap_from_iterator(curr.iter()); - let nsec3 = Nsec3::new(iss.nsec3param.hash_algorithm(), - iss.nsec3param.flags(), - iss.nsec3param.iterations(), - iss.nsec3param.salt().clone(), - nsec3.next_owner().clone(), rtypebitmap); + let nsec3 = Nsec3::new( + iss.nsec3param.hash_algorithm(), + iss.nsec3param.flags(), + iss.nsec3param.iterations(), + iss.nsec3param.salt().clone(), + nsec3.next_owner().clone(), + rtypebitmap, + ); let record = Record::new( - nsec3_record.owner().clone(), - nsec3_record.class(), - nsec3_record.ttl(), - ZoneRecordData::Nsec3(nsec3), + nsec3_record.owner().clone(), + nsec3_record.class(), + nsec3_record.ttl(), + ZoneRecordData::Nsec3(nsec3), ); iss.nsec3s.insert(owner.clone(), record); @@ -2934,9 +2925,12 @@ println!("nsec3_update_bitmap: for name {name} record {nsec3_record:?} curr {cur curr } -fn nsec3_remove_full(name: &Name, nsec3_name: &Name, - nsec3_next: &OwnerHash, iss: &mut IncrementalSigningState) { - println!("nsec3_remove_full: for name {name} nsec3 name {nsec3_name}"); +fn nsec3_remove_full( + name: &Name, + nsec3_name: &Name, + nsec3_next: &OwnerHash, + iss: &mut IncrementalSigningState, +) { nsec3_remove_one(nsec3_name, nsec3_next, iss); // Assume that we never remove the APEX. So the parent always exists. @@ -2948,94 +2942,124 @@ fn nsec3_remove_et(name: &Name, iss: &mut IncrementalSigningState) { // Check if name is an ET. If so remove it and see if the parent is // also an ET. // - // Take a simple approach to check if a name is an ET: first lookup + // Take a simple approach to check if a name is an ET: first lookup // the NSEC3 record for name and check that the bitmap is empty. Then - // check all descendent names and check that none of them has an + // check all descendent names and check that none of them has an // NSEC3 record. - println!("nsec3_remove_et: for name {name}"); let mut name = name.clone(); loop { - if !name.ends_with(&iss.origin) { - // This is weird, we should never be able to get beyond APEX. - // Just ignore this. - return; - } - if name == iss.origin { - // Never remove the NSEC3 record for APEX. - return; - } - - let nsec3_hash_octets = - OwnerHash::::octets_from( - nsec3_hash::<_, _, BytesMut>(&name, - iss.nsec3param.hash_algorithm(), - iss.nsec3param.iterations(), - iss.nsec3param.salt()) - .expect("should not fail") - ); - let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); - let mut builder = NameBuilder::::new(); - builder.append_label(nsec3_hash_base32.as_bytes()).expect("should not fail"); - let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); - println!("nsec3_remove_et: got nsec3 name {nsec3_name} for name {name}"); + if !name.ends_with(&iss.origin) { + // This is weird, we should never be able to get beyond APEX. + // Just ignore this. + return; + } + if name == iss.origin { + // Never remove the NSEC3 record for APEX. + return; + } + + let nsec3_hash_octets = OwnerHash::::octets_from( + nsec3_hash::<_, _, BytesMut>( + &name, + iss.nsec3param.hash_algorithm(), + iss.nsec3param.iterations(), + iss.nsec3param.salt(), + ) + .expect("should not fail"), + ); + let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); + let mut builder = NameBuilder::::new(); + builder + .append_label(nsec3_hash_base32.as_bytes()) + .expect("should not fail"); + let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); let Some(record_nsec3) = iss.nsec3s.get(&nsec3_name) else { - // No NSEC3 record, nothing to do. - return; - }; - - let ZoneRecordData::Nsec3(nsec3) = record_nsec3.data() else { - panic!("NSEC3 record expected"); - }; - - if !nsec3.types().is_empty() { - // There are types here. - return; - } - - // Check the descendents. - let key = (name.clone(), Rtype::SOA); - let range = iss.new_data.range(key..); - let mut opt_curr_name: Option<&Name> = None; - - for ((key_name, _), _) in range { - println!("trying {key_name} for {name}"); - // There is no easy way to avoid name showing up in the range. Just - // filter out name. - if *key_name == name { - continue; - } - - // Make sure curr_name is below name. - if !key_name.ends_with(&name) { - break; - } - - todo!(); - } - - // No descendents with NSEC3 records are found. Delete this one. - let next_owner = nsec3.next_owner().clone(); - nsec3_remove_one(&nsec3_name, &next_owner, iss); - - // We remove the NSEC3 record for the name. Get the parent. We should - // be below APEX, so the parent has to exist. - name = name.parent().expect("parent should exist"); + // No NSEC3 record, nothing to do. + return; + }; + + let ZoneRecordData::Nsec3(nsec3) = record_nsec3.data() else { + panic!("NSEC3 record expected"); + }; + + if !nsec3.types().is_empty() { + // There are types here. + return; + } + + // Check the descendents. + let key = (name.clone(), Rtype::SOA); + let range = iss.new_data.range(key..); + let mut opt_curr_name: Option<&Name> = None; + + for ((key_name, _), _) in range { + // There is no easy way to avoid name showing up in the range. Just + // filter out name. + if *key_name == name { + continue; + } + + // Make sure curr_name is below name. + if !key_name.ends_with(&name) { + break; + } + + if let Some(curr_name) = opt_curr_name { + if key_name == curr_name { + // Already checked. + continue; + } + } + opt_curr_name = Some(key_name); + + let nsec3_hash_octets = OwnerHash::::octets_from( + nsec3_hash::<_, _, BytesMut>( + &key_name, + iss.nsec3param.hash_algorithm(), + iss.nsec3param.iterations(), + iss.nsec3param.salt(), + ) + .expect("should not fail"), + ); + let nsec3_hash_base32 = + base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); + let mut builder = NameBuilder::::new(); + builder + .append_label(nsec3_hash_base32.as_bytes()) + .expect("should not fail"); + let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); + if iss.nsec3s.contains_key(&nsec3_name) { + // NSEC3 record is found. Our target is not an ET. + return; + }; + } + + // No descendents with NSEC3 records are found. Delete this one. + let next_owner = nsec3.next_owner().clone(); + nsec3_remove_one(&nsec3_name, &next_owner, iss); + + // We remove the NSEC3 record for the name. Get the parent. We should + // be below APEX, so the parent has to exist. + name = name.parent().expect("parent should exist"); } } -fn nsec3_remove_one(nsec3_name: &Name, nsec3_next: &OwnerHash, iss: &mut IncrementalSigningState) { - println!("nsec3_remove_one: for {nsec3_name}"); - +fn nsec3_remove_one( + nsec3_name: &Name, + nsec3_next: &OwnerHash, + iss: &mut IncrementalSigningState, +) { // Try to find the NSEC3 record that comes before the one we are trying - // to remove. + // to remove. let mut range = iss.nsec3s.range::, _>(..nsec3_name); - let (previous_name, previous_record) = - if let Some(kv) = range.next_back() { - kv - } else { - let mut range = iss.nsec3s.range::, _>(nsec3_name..); - range.next_back().expect("at least one element should exist") - }; + let (previous_name, previous_record) = if let Some(kv) = range.next_back() { + kv + } else { + let mut range = iss.nsec3s.range::, _>(nsec3_name..); + range + .next_back() + .expect("at least one element should exist") + }; let previous_name = previous_name.clone(); let previous_record = previous_record.clone(); @@ -3043,7 +3067,14 @@ fn nsec3_remove_one(nsec3_name: &Name, nsec3_next: &OwnerHash, iss let ZoneRecordData::Nsec3(previous_nsec) = previous_record.data() else { panic!("NSEC3 record expected"); }; - let previous_nsec3 = Nsec3::new(iss.nsec3param.hash_algorithm(), iss.nsec3param.flags(), iss.nsec3param.iterations(), iss.nsec3param.salt().clone(), nsec3_next.clone(), previous_nsec.types().clone()); + let previous_nsec3 = Nsec3::new( + iss.nsec3param.hash_algorithm(), + iss.nsec3param.flags(), + iss.nsec3param.iterations(), + iss.nsec3param.salt().clone(), + nsec3_next.clone(), + previous_nsec.types().clone(), + ); let previous_record = Record::new( previous_name.clone(), previous_record.class(), @@ -3058,8 +3089,83 @@ fn nsec3_remove_one(nsec3_name: &Name, nsec3_next: &OwnerHash, iss iss.rrsigs.remove(&key); } -fn nsec3_clear_occluded(name: &Name, iss: &mut IncrementalSigningState) -> Result<(), Error> { - println!("nsec3_clear_occluded: for {name}"); +fn nsec3_set_occluded(name: &Name, iss: &mut IncrementalSigningState) { + // Loop over all names below name, if there is an NSEC3 record then + // delete all signatures and the NSEC3 record. + + let key = (name.clone(), Rtype::SOA); + let range = iss.new_data.range(key..); + let mut opt_curr_name: Option<&Name> = None; + let mut work = vec![]; + + for ((key_name, _), _) in range { + // There is no easy way to avoid name showing up in the range. Just + // filter out name. + if key_name == name { + continue; + } + + // Make sure curr_name is below name. + if !key_name.ends_with(name) { + break; + } + + if let Some(curr_name) = opt_curr_name { + if key_name == curr_name { + // Looked at this name already. + continue; + } + } + opt_curr_name = Some(key_name); + + let nsec3_hash_octets = OwnerHash::::octets_from( + nsec3_hash::<_, _, BytesMut>( + &key_name, + iss.nsec3param.hash_algorithm(), + iss.nsec3param.iterations(), + iss.nsec3param.salt(), + ) + .expect("should not fail"), + ); + let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); + let mut builder = NameBuilder::::new(); + builder + .append_label(nsec3_hash_base32.as_bytes()) + .expect("should not fail"); + let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); + let Some(record_nsec3) = iss.nsec3s.get(&nsec3_name) else { + // No NSEC3 record, nothing to do. + continue; + }; + + let ZoneRecordData::Nsec3(nsec3) = record_nsec3.data() else { + panic!("NSEC3 record expected"); + }; + + work.push((key_name.clone(), nsec3_name)); + + // Remove all signatures. + for rtype in nsec3.types().iter() { + let key = (key_name.clone(), rtype); + iss.rrsigs.remove(&key); + } + } + for (key_name, nsec3_name) in work { + let record_nsec3 = iss.nsec3s.get(&nsec3_name).expect("NSEC3 should exist"); + + let ZoneRecordData::Nsec3(nsec3) = record_nsec3.data() else { + panic!("NSEC3 record expected"); + }; + + let nsec3_next = nsec3.next_owner().clone(); + nsec3_remove_full(&key_name, &nsec3_name, &nsec3_next, iss); + } +} + +fn nsec3_clear_occluded( + name: &Name, + iss: &mut IncrementalSigningState, +) -> Result<(), Error> { let key = (name.clone(), Rtype::SOA); let range = iss.new_data.range(key..); let mut opt_curr_name: Option<&Name> = None; @@ -3106,48 +3212,53 @@ fn nsec3_clear_occluded(name: &Name, iss: &mut IncrementalSigningState) - if let Some(curr_name) = opt_curr_name { work.push((curr_name.clone(), curr_types)); } - for (curr_name, curr_types) in work { + for (curr_name, mut curr_types) in work { let mut curr_types = if curr_types.contains(&Rtype::NS) { let has_ds = curr_types.contains(&Rtype::DS); let mut curr_types: HashSet = [Rtype::NS].into(); if has_ds { curr_types.insert(Rtype::DS); + curr_types.insert(Rtype::RRSIG); } curr_types } else { + curr_types.insert(Rtype::RRSIG); curr_types }; let rtypebitmap = nsec3_rtypebitmap_from_iterator(curr_types.iter()); - // Make sure NS doesn't get signed. + // Make sure NS doesn't get signed. And avoid signing RRSIGs. curr_types.remove(&Rtype::NS); + curr_types.remove(&Rtype::RRSIG); sign_rtype_set(&curr_name, &curr_types, iss)?; - let nsec3_hash_octets = - OwnerHash::::octets_from( - nsec3_hash::<_, _, BytesMut>(&curr_name, - iss.nsec3param.hash_algorithm(), - iss.nsec3param.iterations(), - iss.nsec3param.salt()) - .expect("should not fail") - ); - let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); - let mut builder = NameBuilder::::new(); - builder.append_label(nsec3_hash_base32.as_bytes()).expect("should not fail"); - let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); + let nsec3_hash_octets = OwnerHash::::octets_from( + nsec3_hash::<_, _, BytesMut>( + &curr_name, + iss.nsec3param.hash_algorithm(), + iss.nsec3param.iterations(), + iss.nsec3param.salt(), + ) + .expect("should not fail"), + ); + let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); + let mut builder = NameBuilder::::new(); + builder + .append_label(nsec3_hash_base32.as_bytes()) + .expect("should not fail"); + let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); nsec3_insert_full(&curr_name, nsec3_hash_octets, &nsec3_name, rtypebitmap, iss); } Ok(()) } - fn nsec3_rtypebitmap_from_iterator<'a, I>(iter: I) -> RtypeBitmap where I: Iterator, { let mut rtypebitmap = RtypeBitmap::::builder(); for rtype in iter { - rtypebitmap.add(*rtype).expect("should not fail"); + rtypebitmap.add(*rtype).expect("should not fail"); } rtypebitmap.finalize() } @@ -3156,29 +3267,29 @@ fn is_occluded(name: &Name, iss: &IncrementalSigningState) -> bool { // We need to check if the parent of name is a delegation. Stop // when we reached origin. let Some(mut curr) = name.parent() else { - // We asked for the parent of the root. That is weird. Just - // return not occluded. - return false; + // We asked for the parent of the root. That is weird. Just + // return not occluded. + return false; }; loop { - if curr == iss.origin { - // We reached apex. The name was not occluded. - return false; - } - if !curr.ends_with(&iss.origin) { - // Something weird is going on. Return not occluded. - return false; - } - if iss.new_data.contains_key(&(curr.clone(), Rtype::NS)) { - // Name is occluded. - return true; - } - let Some(parent) = curr.parent() else { - // We asked for the parent of the root. That is weird. Just - // return not occluded. - return false; - }; - curr = parent; + if curr == iss.origin { + // We reached apex. The name was not occluded. + return false; + } + if !curr.ends_with(&iss.origin) { + // Something weird is going on. Return not occluded. + return false; + } + if iss.new_data.contains_key(&(curr.clone(), Rtype::NS)) { + // Name is occluded. + return true; + } + let Some(parent) = curr.parent() else { + // We asked for the parent of the root. That is weird. Just + // return not occluded. + return false; + }; + curr = parent; } } @@ -3187,25 +3298,23 @@ fn sign_rtype_set( set: &HashSet, iss: &mut IncrementalSigningState, ) -> Result<(), Error> { - println!("sign_rtype_set: name {name} set {set:?}"); let mut new_sigs = vec![]; for rtype in set { - let key = (name.clone(), *rtype); - let Some(records) = iss.new_data.get(&key) else { - panic!("Expected something for {name}/{rtype}"); - }; - sign_records( - records, - &iss.keys, - iss.inception, - iss.expiration, - &mut new_sigs, - )?; + let key = (name.clone(), *rtype); + let Some(records) = iss.new_data.get(&key) else { + panic!("Expected something for {name}/{rtype}"); + }; + sign_records( + records, + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; } for (sig, rtype) in new_sigs { - let key = (sig[0].owner().clone(), rtype); - println!("sign_rtype_set: rrsig.insert {key:?} {sig:?}"); - iss.rrsigs.insert(key, sig); + let key = (sig[0].owner().clone(), rtype); + iss.rrsigs.insert(key, sig); } Ok(()) } @@ -3219,23 +3328,23 @@ fn sign_records( ) -> Result<(), Error> { let rtype = records[0].rtype(); if rtype == Rtype::DNSKEY || rtype == Rtype::CDS || rtype == Rtype::CDNSKEY { - // These records get signed with the KSK(s). Don't touch - // the signatures. - return Ok(()); + // These records get signed with the KSK(s). Don't touch + // the signatures. + return Ok(()); } let rrset = Rrset::new(records).map_err(|e| format!("Rrset::new failed: {e}"))?; let mut rrsig_records = vec![]; for key in keys { - let rrsig = sign_rrset(key, &rrset, inception, expiration) - .map_err(|e| format!("signing failed: {e}"))?; - let record = Record::new( - rrsig.owner().clone(), - rrsig.class(), - rrsig.ttl(), - ZoneRecordData::Rrsig(rrsig.data().clone()), - ); - rrsig_records.push(record); + let rrsig = sign_rrset(key, &rrset, inception, expiration) + .map_err(|e| format!("signing failed: {e}"))?; + let record = Record::new( + rrsig.owner().clone(), + rrsig.class(), + rrsig.ttl(), + ZoneRecordData::Rrsig(rrsig.data().clone()), + ); + rrsig_records.push(record); } new_sigs.push((rrsig_records, rrset.rtype())); Ok(()) From 1ecc6ce7004c201c5aa34d56b981b0eebe4c6cd4 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 4 Feb 2026 11:44:43 +0100 Subject: [PATCH 09/20] Factor out some common code. --- src/commands/signer.rs | 125 +++++++++++------------------------------ 1 file changed, 34 insertions(+), 91 deletions(-) diff --git a/src/commands/signer.rs b/src/commands/signer.rs index eda9f825..e9d93e2c 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -2364,21 +2364,8 @@ fn incremental_nsec3(iss: &mut IncrementalSigningState) -> Result<(), Error> { // The intersection between add and delete is empty. assert!(add.intersection(delete).next().is_none()); - let nsec3_hash_octets = OwnerHash::::octets_from( - nsec3_hash::<_, _, BytesMut>( - key, - iss.nsec3param.hash_algorithm(), - iss.nsec3param.iterations(), - iss.nsec3param.salt(), - ) - .expect("should not fail"), - ); - let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); - let mut builder = NameBuilder::::new(); - builder - .append_label(nsec3_hash_base32.as_bytes()) - .expect("should not fail"); - let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); + let (nsec3_hash_octets, nsec3_name) = nsec3_hash_parts(key, iss); + if let Some(record_nsec3) = iss.nsec3s.get(&nsec3_name) { let record_nsec3 = record_nsec3.clone(); let ZoneRecordData::Nsec3(nsec3) = record_nsec3.data() else { @@ -2772,21 +2759,8 @@ fn nsec3_insert_ent(name: &Name, iss: &mut IncrementalSigningState) { return; } - let nsec3_hash_octets = OwnerHash::::octets_from( - nsec3_hash::<_, _, BytesMut>( - &name, - iss.nsec3param.hash_algorithm(), - iss.nsec3param.iterations(), - iss.nsec3param.salt(), - ) - .expect("should not fail"), - ); - let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); - let mut builder = NameBuilder::::new(); - builder - .append_label(nsec3_hash_base32.as_bytes()) - .expect("should not fail"); - let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); + let (nsec3_hash_octets, nsec3_name) = nsec3_hash_parts(&name, iss); + if iss.nsec3s.contains_key(&nsec3_name) { // Found something. We are done. return; @@ -2958,21 +2932,8 @@ fn nsec3_remove_et(name: &Name, iss: &mut IncrementalSigningState) { return; } - let nsec3_hash_octets = OwnerHash::::octets_from( - nsec3_hash::<_, _, BytesMut>( - &name, - iss.nsec3param.hash_algorithm(), - iss.nsec3param.iterations(), - iss.nsec3param.salt(), - ) - .expect("should not fail"), - ); - let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); - let mut builder = NameBuilder::::new(); - builder - .append_label(nsec3_hash_base32.as_bytes()) - .expect("should not fail"); - let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); + let (_, nsec3_name) = nsec3_hash_parts(&name, iss); + let Some(record_nsec3) = iss.nsec3s.get(&nsec3_name) else { // No NSEC3 record, nothing to do. return; @@ -3012,22 +2973,8 @@ fn nsec3_remove_et(name: &Name, iss: &mut IncrementalSigningState) { } opt_curr_name = Some(key_name); - let nsec3_hash_octets = OwnerHash::::octets_from( - nsec3_hash::<_, _, BytesMut>( - &key_name, - iss.nsec3param.hash_algorithm(), - iss.nsec3param.iterations(), - iss.nsec3param.salt(), - ) - .expect("should not fail"), - ); - let nsec3_hash_base32 = - base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); - let mut builder = NameBuilder::::new(); - builder - .append_label(nsec3_hash_base32.as_bytes()) - .expect("should not fail"); - let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); + let (_, nsec3_name) = nsec3_hash_parts(key_name, iss); + if iss.nsec3s.contains_key(&nsec3_name) { // NSEC3 record is found. Our target is not an ET. return; @@ -3118,21 +3065,8 @@ fn nsec3_set_occluded(name: &Name, iss: &mut IncrementalSigningState) { } opt_curr_name = Some(key_name); - let nsec3_hash_octets = OwnerHash::::octets_from( - nsec3_hash::<_, _, BytesMut>( - &key_name, - iss.nsec3param.hash_algorithm(), - iss.nsec3param.iterations(), - iss.nsec3param.salt(), - ) - .expect("should not fail"), - ); - let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); - let mut builder = NameBuilder::::new(); - builder - .append_label(nsec3_hash_base32.as_bytes()) - .expect("should not fail"); - let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); + let (_, nsec3_name) = nsec3_hash_parts(key_name, iss); + let Some(record_nsec3) = iss.nsec3s.get(&nsec3_name) else { // No NSEC3 record, nothing to do. continue; @@ -3232,21 +3166,8 @@ fn nsec3_clear_occluded( curr_types.remove(&Rtype::RRSIG); sign_rtype_set(&curr_name, &curr_types, iss)?; - let nsec3_hash_octets = OwnerHash::::octets_from( - nsec3_hash::<_, _, BytesMut>( - &curr_name, - iss.nsec3param.hash_algorithm(), - iss.nsec3param.iterations(), - iss.nsec3param.salt(), - ) - .expect("should not fail"), - ); - let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); - let mut builder = NameBuilder::::new(); - builder - .append_label(nsec3_hash_base32.as_bytes()) - .expect("should not fail"); - let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); + let (nsec3_hash_octets, nsec3_name) = nsec3_hash_parts(&curr_name, iss); + nsec3_insert_full(&curr_name, nsec3_hash_octets, &nsec3_name, rtypebitmap, iss); } Ok(()) @@ -3263,6 +3184,28 @@ where rtypebitmap.finalize() } +fn nsec3_hash_parts( + name: &Name, + iss: &IncrementalSigningState, +) -> (OwnerHash, Name) { + let nsec3_hash_octets = OwnerHash::::octets_from( + nsec3_hash::<_, _, BytesMut>( + name, + iss.nsec3param.hash_algorithm(), + iss.nsec3param.iterations(), + iss.nsec3param.salt(), + ) + .expect("should not fail"), + ); + let nsec3_hash_base32 = base32::encode_string_hex(&nsec3_hash_octets).to_ascii_lowercase(); + let mut builder = NameBuilder::::new(); + builder + .append_label(nsec3_hash_base32.as_bytes()) + .expect("should not fail"); + let nsec3_name = builder.append_origin(&iss.origin).expect("should not fail"); + (nsec3_hash_octets, nsec3_name) +} + fn is_occluded(name: &Name, iss: &IncrementalSigningState) -> bool { // We need to check if the parent of name is a delegation. Stop // when we reached origin. From 357cf39265917daba836478366597d89703cb2f4 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 4 Feb 2026 16:51:43 +0100 Subject: [PATCH 10/20] Support for NSEC3 opt-out. --- Cargo.lock | 2 ++ Cargo.toml | 3 -- src/commands/signer.rs | 79 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 72 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb787d59..647ceac3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -358,6 +358,7 @@ dependencies = [ [[package]] name = "domain" version = "0.11.1" +source = "git+https://github.com/NLnetLabs/domain.git?branch=main#3172a68fe3c6734d3b083e6475d65c563b3e7f23" dependencies = [ "arc-swap", "bumpalo", @@ -403,6 +404,7 @@ dependencies = [ [[package]] name = "domain-macros" version = "0.11.1" +source = "git+https://github.com/NLnetLabs/domain.git?branch=main#3172a68fe3c6734d3b083e6475d65c563b3e7f23" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 40525bcb..4398abb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,6 +142,3 @@ assets = [ # Set Obsoletes per https://docs.fedoraproject.org/en-US/packaging-guidelines/#renaming-or-replacing-existing-packages. [package.metadata.generate-rpm.obsoletes] ldns-utils = "< 0:1.8.4-2" - -[patch.'https://github.com/NLnetLabs/domain.git'] -domain = { path = "../domain" } diff --git a/src/commands/signer.rs b/src/commands/signer.rs index e9d93e2c..5f6c9a32 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -1274,11 +1274,7 @@ impl Signer { initial_diffs(&mut iss)?; if sc.use_nsec3 { - if sc.opt_out { - todo!(); - } else { - incremental_nsec3(&mut iss)?; - } + incremental_nsec3(&mut iss)?; } else { incremental_nsec(&mut iss)?; } @@ -2359,6 +2355,8 @@ fn incremental_nsec3(iss: &mut IncrementalSigningState) -> Result<(), Error> { // However, we removing a delegation, the situation is reversed. For now // assuming that sorting is not necessary. + let opt_out_flag = iss.nsec3param.opt_out_flag(); + let changes = iss.changes.clone(); for (key, (add, delete)) in &changes { // The intersection between add and delete is empty. @@ -2469,6 +2467,19 @@ fn incremental_nsec3(iss: &mut IncrementalSigningState) -> Result<(), Error> { } else { if add.is_empty() { assert!(!delete.is_empty()); + + // Special magic for out-out. It is possible that an NS + // record got deleted. With opt-out there will not be an + // NSEC3 record if there is only a NS record and no DS record. + if opt_out_flag && delete.contains(&Rtype::NS) { + if is_occluded(key, iss) { + // No need to do anything. + continue; + } + nsec3_clear_occluded(key, iss)?; + continue; + } + // No need to do anything. continue; } @@ -2478,7 +2489,29 @@ fn incremental_nsec3(iss: &mut IncrementalSigningState) -> Result<(), Error> { continue; } + // Just copy add in case we need to change it. + let mut add = add.clone(); + if opt_out_flag { + // We have a new record and no NSEC3 record exists. But in the + // case of opt-out there may already be an NS record. + // We are not at APEX because APEX always has an NSEC3 + // record. + let tmpkey = (key.clone(), Rtype::NS); + if iss.new_data.contains_key(&tmpkey) { + // Found an NS record. It is safe to add NS to the add + // set. + add.insert(Rtype::NS); + } + } + if add.contains(&Rtype::NS) { + if opt_out_flag { + // Check if this is just an NS record. If so, don't + // create an NSEC3 record. + if !add.iter().any(|r| *r != Rtype::NS) { + continue; + } + } // Create a new NSEC3 record and sign only DS records (if any). // If add contains DS then add RRSIG to add. @@ -2503,7 +2536,7 @@ fn incremental_nsec3(iss: &mut IncrementalSigningState) -> Result<(), Error> { // Create a new NSEC3 record and sign all records. let rtypebitmap = nsec3_rtypebitmap_from_iterator(add_with_rrsig.iter()); nsec3_insert_full(key, nsec3_hash_octets, &nsec3_name, rtypebitmap, iss); - sign_rtype_set(key, add, iss)?; + sign_rtype_set(key, &add, iss)?; } } Ok(()) @@ -2874,11 +2907,40 @@ fn nsec3_update_bitmap( } if curr.is_empty() { - nsec3_remove_full(name, owner, nsec3.next_owner(), iss); + // The NSEC3 bitmp will be empty, but this may now have become an + // empty non-terminal. Our only option is to update the NSEC3 record + // and then call nsec3_remove_et to see if it is empty can can be + // removed. + nsec3_update(owner, nsec3_record, nsec3, &curr, iss); + nsec3_remove_et(name, iss); return curr; } - let rtypebitmap = nsec3_rtypebitmap_from_iterator(curr.iter()); + if iss.nsec3param.opt_out_flag() && !curr.iter().any(|r| *r != Rtype::NS) { + // The new bitmap has nothing except for NS. We would like to delete + // the NSEC3. However there may still be descendents that need to be + // removed with nsec3_set_occluded. Update this NSEC3 to be empty and + // call nsec3_remove_et to remove it if there are no descendents. + + let empty_curr = HashSet::new(); + nsec3_update(owner, nsec3_record, nsec3, &empty_curr, iss); + nsec3_remove_et(name, iss); + return curr; + } + + nsec3_update(owner, nsec3_record, nsec3, &curr, iss); + curr +} + +fn nsec3_update( + owner: &Name, + nsec3_record: &Zrd, + nsec3: &Nsec3, + rtypes: &HashSet, + iss: &mut IncrementalSigningState, +) { + // Just update an NSEC3 record without further logic. + let rtypebitmap = nsec3_rtypebitmap_from_iterator(rtypes.iter()); let nsec3 = Nsec3::new( iss.nsec3param.hash_algorithm(), iss.nsec3param.flags(), @@ -2896,7 +2958,6 @@ fn nsec3_update_bitmap( iss.nsec3s.insert(owner.clone(), record); iss.modified_nsecs.insert(owner.clone()); - curr } fn nsec3_remove_full( From 10f67c7163ed4ce7a1305345f9488f3cc1cd3ac9 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 11 Feb 2026 16:02:32 +0100 Subject: [PATCH 11/20] Initial incremental refresh of signatures and key rolls. --- src/commands/signer.rs | 1223 ++++++++++++++++++++++++++-------------- 1 file changed, 792 insertions(+), 431 deletions(-) diff --git a/src/commands/signer.rs b/src/commands/signer.rs index 5f6c9a32..86a69b6c 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -63,6 +63,7 @@ use url::Url; const FOUR_WEEKS: u64 = 2419200; const TWO_WEEKS: u64 = 1209600; +const FIFTEEN_MINUTES: u64 = 15 * 60; //------------ Signer -------------------------------------------------------- @@ -368,6 +369,8 @@ impl Signer { zonemd: HashSet::new(), serial_policy: SerialPolicy::Keep, notify_command: Vec::new(), + signature_refresh_interval: Duration::from_secs(FIFTEEN_MINUTES), + key_roll_time: Duration::from_secs(ONE_DAY), faketime: None, }; let json = serde_json::to_string_pretty(&sc).expect("should not fail"); @@ -390,6 +393,11 @@ impl Signer { zonefile_modified: UNIX_EPOCH.try_into().expect("should not fail"), minimum_expiration: UNIX_EPOCH.try_into().expect("should not fail"), previous_serial: None, + apex_remove: HashSet::new(), + apex_extra: vec![], + key_tags: HashSet::new(), + key_roll: None, + last_signature_refresh: UNIX_EPOCH.try_into().expect("should not fail"), }; let json = serde_json::to_string_pretty(&signer_state).expect("should not fail"); let mut file = File::create(&signer_state_file).map_err(|e| { @@ -411,16 +419,15 @@ impl Signer { let file = File::open(&self.signer_config) .map_err(|e| format!("unable to open file {}: {e}", self.signer_config.display()))?; - let mut sc: SignerConfig = serde_json::from_reader(file).map_err::(|e| { + let sc: SignerConfig = serde_json::from_reader(file).map_err::(|e| { format!("error loading {:?}: {e}\n", self.signer_config).into() })?; let file = File::open(&sc.signer_state) .map_err(|e| format!("unable to open file {}: {e}", sc.signer_state.display()))?; - let mut signer_state: SignerState = - serde_json::from_reader(file).map_err::(|e| { - format!("error loading {:?}: {e}\n", sc.signer_state).into() - })?; + let signer_state: SignerState = serde_json::from_reader(file).map_err::(|e| { + format!("error loading {:?}: {e}\n", sc.signer_state).into() + })?; let keyset_state_modified = Self::file_modified(sc.keyset_state.clone())?; let file = File::open(&sc.keyset_state) @@ -429,8 +436,15 @@ impl Signer { format!("error loading {:?}: {e}\n", sc.keyset_state).into() })?; - let mut config_changed = false; - let mut state_changed = false; + let mut ws = WorkSpace { + keyset_state: kss, + keyset_state_modified, + config: sc, + config_changed: false, + state: signer_state, + state_changed: false, + }; + let mut res = Ok(()); match self.cmd { @@ -455,78 +469,49 @@ impl Signer { // Copy modified times to the state file. Do we need to be clever // and avoid updating the state file if modified times do not // change? - signer_state.config_modified = signer_config_modified; - signer_state.keyset_state_modified = keyset_state_modified; - let zonefile_modified = Self::file_modified(sc.zonefile_in.clone())?; - signer_state.zonefile_modified = zonefile_modified; - state_changed = true; - res = self.go_further(env, &sc, &mut signer_state, &kss, options) - } - Commands::Resign => self.resign(&sc, &kss, env)?, + ws.state.config_modified = signer_config_modified; + let zonefile_modified = Self::file_modified(ws.config.zonefile_in.clone())?; + ws.state.zonefile_modified = zonefile_modified; + ws.state_changed = true; + res = self.sign_full(&mut ws, env, options) + } + Commands::Resign => ws.resign()?, Commands::Show => { todo!(); } Commands::Cron => { - // Simple automatic signer. Re-sign the zone when needed. - // The zone needs to be signed when one or more of the three - // input files has changed (signer config, keyset state or the - // unsigned zone file. - // The zone also needs to be signed when the remaining signature - // lifetime is not long enough anymore. - - let mut need_resign = false; - if signer_config_modified != signer_state.config_modified { - need_resign = true; - } - if keyset_state_modified != signer_state.keyset_state_modified { - need_resign = true; - } - let zonefile_modified = Self::file_modified(sc.zonefile_in.clone())?; - if zonefile_modified != signer_state.zonefile_modified { - need_resign = true; - } - let now = UnixTime::now(); - if now + sc.minimal_remaining_validity > signer_state.minimum_expiration { - todo!(); - } - if need_resign { - signer_state.config_modified = signer_config_modified; - signer_state.keyset_state_modified = keyset_state_modified; - let zonefile_modified = Self::file_modified(sc.zonefile_in.clone())?; - signer_state.zonefile_modified = zonefile_modified; - state_changed = true; - res = self.go_further(env, &sc, &mut signer_state, &kss, Default::default()) - } - } - Commands::Set { subcommand } => { - set_command(subcommand, &mut sc, &mut config_changed, &env)? + ws.sign_incrementally(false)?; } + Commands::Set { subcommand } => ws.set_command(subcommand, &env)?, } - if config_changed { - let json = serde_json::to_string_pretty(&sc).expect("should not fail"); + if ws.config_changed { + let json = serde_json::to_string_pretty(&ws.config).expect("should not fail"); let mut file = File::create(&self.signer_config) .map_err(|e| format!("unable to create {}: {e}", self.signer_config.display()))?; write!(file, "{json}") .map_err(|e| format!("unable to write to {}: {e}", self.signer_config.display()))?; } - if state_changed { - let json = serde_json::to_string_pretty(&signer_state).expect("should not fail"); - let mut file = File::create(&sc.signer_state) - .map_err(|e| format!("unable to create {}: {e}", sc.signer_state.display()))?; - write!(file, "{json}") - .map_err(|e| format!("unable to write to {}: {e}", sc.signer_state.display()))?; + if ws.state_changed { + let json = serde_json::to_string_pretty(&ws.state).expect("should not fail"); + let mut file = File::create(&ws.config.signer_state).map_err(|e| { + format!("unable to create {}: {e}", ws.config.signer_state.display()) + })?; + write!(file, "{json}").map_err(|e| { + format!( + "unable to write to {}: {e}", + ws.config.signer_state.display() + ) + })?; } res } #[allow(clippy::too_many_arguments)] - fn go_further( + fn sign_full( &self, + ws: &mut WorkSpace, env: impl Env, - sc: &SignerConfig, - signer_state: &mut SignerState, - kss: &KeySetState, options: SigningOptions, ) -> Result<(), Error> { let signing_mode = if options.hash_only { @@ -535,11 +520,17 @@ impl Signer { SigningMode::HashAndSign }; + // Populate the signer state fields from keyset state. + ws.handle_keyset_changed(); + + // The entire zone is signed, clear key_roll. + ws.state.key_roll = None; + // Read the zone file. - let origin = kss.keyset.name().to_bytes(); - let mut records = self.load_zone(&env.in_cwd(&sc.zonefile_in), origin.clone())?; + let origin = ws.keyset_state.keyset.name().to_bytes(); + let mut records = self.load_zone(&env.in_cwd(&ws.config.zonefile_in), origin.clone())?; - for r in &kss.dnskey_rrset { + for r in &ws.keyset_state.dnskey_rrset { let zonefile = domain::zonefile::inplace::Zonefile::from((r.to_string() + "\n").as_ref() as &str); for entry in zonefile { @@ -557,7 +548,7 @@ impl Signer { records.insert(r).unwrap(); } } - for r in &kss.cds_rrset { + for r in &ws.keyset_state.cds_rrset { let zonefile = domain::zonefile::inplace::Zonefile::from((r.to_string() + "\n").as_ref() as &str); for entry in zonefile { @@ -577,7 +568,7 @@ impl Signer { } let mut keys = Vec::new(); - for (k, v) in kss.keyset.keys() { + for (k, v) in ws.keyset_state.keyset.keys() { let signer = match v.keytype() { KeyType::Ksk(_) => false, KeyType::Zsk(key_state) => key_state.signer(), @@ -626,7 +617,7 @@ impl Signer { } let signing_keys: Vec<_> = keys.iter().collect(); - let out_file = sc.zonefile_out.clone(); + let out_file = ws.config.zonefile_out.clone(); let mut writer = if out_file.as_os_str() == "-" { FileOrStdout::Stdout(env.stdout()) @@ -642,7 +633,7 @@ impl Signer { }; // Make sure, zonemd arguments are unique - let zonemd: HashSet = HashSet::from_iter(sc.zonemd.clone()); + let zonemd: HashSet = HashSet::from_iter(ws.config.zonemd.clone()); // implement SOA serial policies. There are four policies: // 1) Keep. Copy the serial from the unsigned zone. Refuse to sign @@ -662,9 +653,9 @@ impl Signer { unreachable!(); }; - match sc.serial_policy { + match ws.config.serial_policy { SerialPolicy::Keep => { - if let Some(previous_serial) = signer_state.previous_serial { + if let Some(previous_serial) = ws.state.previous_serial { if zone_soa.serial() <= previous_serial { return Err( "Serial policy is Keep but upstream serial did not increase".into() @@ -672,11 +663,11 @@ impl Signer { } } - signer_state.previous_serial = Some(zone_soa.serial()); + ws.state.previous_serial = Some(zone_soa.serial()); } SerialPolicy::Increment => { let mut serial = zone_soa.serial(); - if let Some(previous_serial) = signer_state.previous_serial { + if let Some(previous_serial) = ws.state.previous_serial { if serial <= previous_serial { serial = previous_serial.add(1); @@ -693,11 +684,11 @@ impl Signer { records.update_data(|rr| rr.rtype() == Rtype::SOA, new_soa); } } - signer_state.previous_serial = Some(serial); + ws.state.previous_serial = Some(serial); } SerialPolicy::UnixSeconds => { let mut serial = Serial::now(); - if let Some(previous_serial) = signer_state.previous_serial { + if let Some(previous_serial) = ws.state.previous_serial { if serial <= previous_serial { serial = previous_serial.add(1); } @@ -714,7 +705,7 @@ impl Signer { )); records.update_data(|rr| rr.rtype() == Rtype::SOA, new_soa); - signer_state.previous_serial = Some(serial); + ws.state.previous_serial = Some(serial); } SerialPolicy::Date => { let ts = JiffTimestamp::now(); @@ -724,7 +715,7 @@ impl Signer { * 100; let mut serial: Serial = serial.into(); - if let Some(previous_serial) = signer_state.previous_serial { + if let Some(previous_serial) = ws.state.previous_serial { if serial <= previous_serial { serial = previous_serial.add(1); } @@ -741,7 +732,7 @@ impl Signer { )); records.update_data(|rr| rr.rtype() == Rtype::SOA, new_soa); - signer_state.previous_serial = Some(serial); + ws.state.previous_serial = Some(serial); } } @@ -783,7 +774,7 @@ impl Signer { let mut nsec3_hashes: Option = None; - if sc.use_nsec3 && (options.extra_comments || options.preceed_zone_with_hash_list) { + if ws.config.use_nsec3 && (options.extra_comments || options.preceed_zone_with_hash_list) { // Create a collection of NSEC3 hashes that can later be used for // debug output. let mut hash_provider = Nsec3HashMap::new(); @@ -816,7 +807,7 @@ impl Signer { if rrset.rtype() == Rtype::NS && *owner != apex { delegation = Some(owner.clone()); - if sc.opt_out { + if ws.config.opt_out { // Delegations are ignored for NSEC3. Ignore this // entry but keep looking for other types at the // same owner name. @@ -825,9 +816,14 @@ impl Signer { } } - let hashed_name = - mk_hashed_nsec3_owner_name(owner, sc.algorithm, sc.iterations, &sc.salt, &apex) - .map_err(|err| Error::from(format!("NSEC3 error: {err}")))?; + let hashed_name = mk_hashed_nsec3_owner_name( + owner, + ws.config.algorithm, + ws.config.iterations, + &ws.config.salt, + &apex, + ) + .map_err(|err| Error::from(format!("NSEC3 error: {err}")))?; let hash_info = Nsec3HashInfo::new(owner.clone(), false); hash_provider .hashes_by_unhashed_owner @@ -852,9 +848,9 @@ impl Signer { let hashed_name = mk_hashed_nsec3_owner_name( &suffix, - sc.algorithm, - sc.iterations, - &sc.salt, + ws.config.algorithm, + ws.config.iterations, + &ws.config.salt, &apex, ) .map_err(|err| Error::from(format!("NSEC3 error: {err}")))?; @@ -876,10 +872,13 @@ impl Signer { nsec3_hashes = Some(hash_provider); } - let now = - Into::::into(sc.faketime.clone().unwrap_or(UnixTime::now())).as_secs() as u32; - let inception = (now - sc.inception_offset.as_secs() as u32).into(); - let expiration = (now + sc.signature_lifetime.as_secs() as u32).into(); + let now = ws.faketime_or_now(); + let now_u32 = Into::::into(now.clone()).as_secs() as u32; + let inception = (now_u32 - ws.config.inception_offset.as_secs() as u32).into(); + let expiration = (now_u32 + ws.config.signature_lifetime.as_secs() as u32).into(); + + // Set last_signature_refresh to the current time. + ws.state.last_signature_refresh = now; let signing_config: SigningConfig<_, _> = match signing_mode { SigningMode::HashOnly | SigningMode::HashAndSign => { @@ -891,10 +890,15 @@ impl Signer { // Note: Assuming that we want to later be able to support // transition between NSEC <-> NSEC3 we will need to be able to // sign with more than one hashing configuration at once. - if sc.use_nsec3 { - let params = Nsec3param::new(sc.algorithm, 0, sc.iterations, sc.salt.clone()); + if ws.config.use_nsec3 { + let params = Nsec3param::new( + ws.config.algorithm, + 0, + ws.config.iterations, + ws.config.salt.clone(), + ); let mut nsec3_config = GenerateNsec3Config::new(params); - if sc.opt_out { + if ws.config.opt_out { nsec3_config = nsec3_config.with_opt_out(); } SigningConfig::new(DenialConfig::Nsec3(nsec3_config), inception, expiration) @@ -946,7 +950,7 @@ impl Signer { // Note that truncating the u64 from as_secs() to u32 is fine because // Timestamp is designed for this situation. let expire_ts: Timestamp = (Duration::from_secs(now_ts.into_int() as u64) - .saturating_add(sc.signature_lifetime) + .saturating_add(ws.config.signature_lifetime) .as_secs() as u32) .into(); let ts = records @@ -984,9 +988,9 @@ impl Signer { let minimum_expiration = if let Some(ts) = ts { ts.into() } else { - UnixTime::now() + sc.signature_lifetime + UnixTime::now() + ws.config.signature_lifetime }; - signer_state.minimum_expiration = minimum_expiration; + ws.state.minimum_expiration = minimum_expiration; // The signed RRs are in DNSSEC canonical order by owner name. For // compatibility with ldns-signzone, re-order them to be in canonical @@ -1143,14 +1147,14 @@ impl Signer { writer.flush().map_err(|e| format!("flush failed: {e}"))?; - if !sc.notify_command.is_empty() { - let output = Command::new(&sc.notify_command[0]) - .args(&sc.notify_command[1..]) + if !ws.config.notify_command.is_empty() { + let output = Command::new(&ws.config.notify_command[0]) + .args(&ws.config.notify_command[1..]) .output() .map_err(|e| { format!( "unable to create new Command for {}: {e}", - sc.notify_command[0] + ws.config.notify_command[0] ) })?; if !output.status.success() { @@ -1167,202 +1171,6 @@ impl Signer { Ok(()) } - fn resign(&self, sc: &SignerConfig, kss: &KeySetState, _env: impl Env) -> Result<(), Error> { - let origin = kss.keyset.name(); - let origin_bytes = Name::::octets_from(origin.clone()); - let mut iss = IncrementalSigningState::new(origin_bytes, sc); - - let mut keys = Vec::new(); - for (k, v) in kss.keyset.keys() { - let signer = match v.keytype() { - KeyType::Ksk(_) => false, - KeyType::Zsk(key_state) => key_state.signer(), - KeyType::Csk(_, key_state) => key_state.signer(), - KeyType::Include(_) => false, - }; - - if signer { - let privref = v.privref().ok_or("missing private key")?; - let priv_url = Url::parse(privref).expect("valid URL expected"); - let private_data = if priv_url.scheme() == "file" { - std::fs::read_to_string(priv_url.path()).map_err::(|e| { - format!("unable read from file {}: {e}", priv_url.path()).into() - })? - } else { - panic!("unsupported URL scheme in {priv_url}"); - }; - let secret_key = SecretKeyBytes::parse_from_bind(&private_data) - .map_err::(|e| { - format!("unable to parse private key file {privref}: {e}").into() - })?; - let pub_url = Url::parse(k).expect("valid URL expected"); - let public_data = if pub_url.scheme() == "file" { - std::fs::read_to_string(pub_url.path()).map_err::(|e| { - format!("unable read from file {}: {e}", pub_url.path()).into() - })? - } else { - panic!("unsupported URL scheme in {pub_url}"); - }; - let public_key = - parse_from_bind::(&public_data).map_err::(|e| { - format!("unable to parse public key file {k}: {e}").into() - })?; - - let key_pair = KeyPair::from_bytes(&secret_key, public_key.data()) - .map_err::(|e| { - format!("private key {privref} and public key {k} do not match: {e}").into() - })?; - let signing_key = SigningKey::new( - public_key.owner().clone(), - public_key.data().flags(), - key_pair, - ); - keys.push(signing_key); - } - } - - iss.keys = keys; - - let start = Instant::now(); - load_signed_zone(&mut iss, &sc.zonefile_out).unwrap(); - println!("loading signed zone took {:?}", start.elapsed()); - - // Note that we could try to regenerate the NSEC(3). Assume that - // switching between NSEC, NSEC3, and NSEC3 opt-out (or other NSEC3 - // parameter changes) is rare enough that we can just resign the full - // zone. - let key = (iss.origin.clone(), Rtype::NSEC3PARAM); - let opt_nsec3param = iss.old_data.get(&key); - if let Some(nsec3param_records) = opt_nsec3param { - // Zone was signed with NSEC3. - if !sc.use_nsec3 { - // Zone is signed with NSEC3 but we want NSEC. - todo!(); - } - let ZoneRecordData::Nsec3param(nsec3param) = nsec3param_records[0].data() else { - panic!("ZoneRecordData::Nsec3param expected"); - }; - if nsec3param.hash_algorithm() != sc.algorithm - || nsec3param.opt_out_flag() != sc.opt_out - || nsec3param.iterations() != sc.iterations - || *nsec3param.salt() != sc.salt - { - // Parameters changed, resign. - todo!(); - } - - // Nothing has changed. Insert the old NSEC3PARAM records in the - // new zone data. - iss.new_data.insert(key, nsec3param_records.to_vec()); - } else { - // Zone was signed with NSEC, check if that is also the target. - if sc.use_nsec3 { - // Resign the full zone with NSEC3. - todo!(); - } - - // Stay with NSEC. - } - - let start = Instant::now(); - load_unsigned_zone(&mut iss, &sc.zonefile_in).unwrap(); - println!("loading new unsigned zone took {:?}", start.elapsed()); - - let start = Instant::now(); - load_apex_records(kss, &mut iss)?; - - initial_diffs(&mut iss)?; - - if sc.use_nsec3 { - incremental_nsec3(&mut iss)?; - } else { - incremental_nsec(&mut iss)?; - } - - let mut new_sigs = vec![]; - if sc.use_nsec3 { - for m in &iss.modified_nsecs { - let Some(nsec3) = iss.nsec3s.get(m) else { - panic!("NSEC3 for {m} should exist"); - }; - - let nsec3 = nsec3.clone(); - sign_records( - &[nsec3], - &iss.keys, - iss.inception, - iss.expiration, - &mut new_sigs, - )?; - } - } else { - for m in &iss.modified_nsecs { - let Some(nsec) = iss.nsecs.get(m) else { - panic!("NSEC for {m} should exist"); - }; - - let nsec = nsec.clone(); - sign_records( - &[nsec], - &iss.keys, - iss.inception, - iss.expiration, - &mut new_sigs, - )?; - } - } - for (sig, rtype) in new_sigs { - let key = (sig[0].owner().clone(), rtype); - iss.rrsigs.insert(key, sig); - } - println!("incremental signing took {:?}", start.elapsed()); - - /* - let mut writer = stdout(); - */ - - let start = Instant::now(); - let mut writer = { - let filename = sc.zonefile_out.as_os_str().to_str().unwrap(); - let filename = filename.to_owned() + "-incremental"; - let file = File::create(&filename) - .map_err(|e| format!("unable to create file {filename}: {e}",))?; - BufWriter::new(file) - // FileOrStdout::File(file) - }; - - for (_, data) in iss.new_data { - for rr in data { - writer - .write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))) - .map_err(|e| format!("unable write signed zone: {e}"))?; - } - } - for (_, rr) in iss.nsecs { - writer - .write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))) - .map_err(|e| format!("unable write signed zone: {e}"))?; - } - for (_, rr) in iss.nsec3s { - writer - .write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))) - .map_err(|e| format!("unable write signed zone: {e}"))?; - } - for (_, data) in iss.rrsigs { - for rr in data { - let ZoneRecordData::Rrsig(rrsig) = rr.data() else { - panic!("RRSIG expected"); - }; - let rr = Record::new(rr.owner(), rr.class(), rr.ttl(), YyyyMmDdHhMMSsRrsig(rrsig)); - writer - .write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))) - .map_err(|e| format!("unable write signed zone: {e}"))?; - } - } - println!("writing output took {:?}", start.elapsed()); - todo!(); - } - fn write_rr>( &self, writer: &mut W, @@ -1715,69 +1523,606 @@ impl Signer { } } -fn set_command( - cmd: SetCommands, - sc: &mut SignerConfig, - config_changed: &mut bool, - env: &impl Env, -) -> Result<(), Error> { - match cmd { - SetCommands::InceptionOffset { duration } => { - sc.inception_offset = duration; - } - SetCommands::Lifetime { duration } => { - sc.signature_lifetime = duration; - } - SetCommands::UseNsec3 { boolean } => { - sc.use_nsec3 = boolean; - } - SetCommands::Algorithm { algorithm } => { - sc.algorithm = algorithm; - } - SetCommands::Iterations { iterations } => { - sc.iterations = iterations; - match sc.iterations { - 500.. => Signer::write_extreme_iterations_warning(&env), - 1.. => Signer::write_non_zero_iterations_warning(&env), - _ => { /* Good, nothing to warn about */ } +struct WorkSpace { + keyset_state: KeySetState, + keyset_state_modified: UnixTime, + config: SignerConfig, + config_changed: bool, + state: SignerState, + state_changed: bool, +} + +impl WorkSpace { + fn set_command(&mut self, cmd: SetCommands, env: &impl Env) -> Result<(), Error> { + match cmd { + SetCommands::InceptionOffset { duration } => { + self.config.inception_offset = duration; + } + SetCommands::Lifetime { duration } => { + self.config.signature_lifetime = duration; + } + SetCommands::UseNsec3 { boolean } => { + self.config.use_nsec3 = boolean; + } + SetCommands::Algorithm { algorithm } => { + self.config.algorithm = algorithm; + } + SetCommands::Iterations { iterations } => { + self.config.iterations = iterations; + match self.config.iterations { + 500.. => Signer::write_extreme_iterations_warning(&env), + 1.. => Signer::write_non_zero_iterations_warning(&env), + _ => { /* Good, nothing to warn about */ } + } + } + SetCommands::Salt { salt } => { + self.config.salt = salt; + } + SetCommands::OptOut { boolean } => { + self.config.opt_out = boolean; + } + SetCommands::ZoneMD { zonemd } => { + self.config.zonemd = zonemd; + } + SetCommands::SerialPolicy { serial_policy } => { + self.config.serial_policy = serial_policy; + } + SetCommands::NotifyCommand { args } => { + self.config.notify_command = args; } + SetCommands::FakeTime { opt_unixtime } => self.config.faketime = opt_unixtime, } - SetCommands::Salt { salt } => { - sc.salt = salt; + self.config_changed = true; + Ok(()) + } + + fn sign_incrementally(&mut self, load_unsigned: bool) -> Result<(), Error> { + // Check what work needs to be done. If the keyset state + // changed then check if the APEX records change or if a + // CSK or ZSK roll require resigning the zone. + // If enough time has passed since the last time + // signatures have been updated, then update signatures + // and during a key roll, sign with the new key(s). + // Ignore signer configuration changes, they will get picked up when + // signatures need to be updated. + // Resign using the unsigned zonefile when load_unsigned is true. + + let apex_changed = self.handle_keyset_changed(); + + let mut refresh_signatures = false; + let now = self.faketime_or_now(); + if now > self.state.last_signature_refresh.clone() + self.config.signature_refresh_interval + { + println!( + "refresh signatures: {} > {} + {:?}", + now, self.state.last_signature_refresh, self.config.signature_refresh_interval + ); + refresh_signatures = true; } - SetCommands::OptOut { boolean } => { - sc.opt_out = boolean; + + if !load_unsigned && !apex_changed && !refresh_signatures { + // Nothing to do. + return Ok(()); } - SetCommands::ZoneMD { zonemd } => { - sc.zonemd = zonemd; + + let mut iss = IncrementalSigningState::new(self)?; + + let start = Instant::now(); + load_signed_zone(&mut iss, &self.config.zonefile_out).unwrap(); + println!("loading signed zone took {:?}", start.elapsed()); + + self.handle_nsec_nsec3(&mut iss); + + if load_unsigned { + let start = Instant::now(); + load_unsigned_zone(&mut iss, &self.config.zonefile_in).unwrap(); + println!("loading new unsigned zone took {:?}", start.elapsed()); + } else { + // Re-use the signed data. + load_signed_only(&mut iss); } - SetCommands::SerialPolicy { serial_policy } => { - sc.serial_policy = serial_policy; + + let start = Instant::now(); + self.load_apex_records(&mut iss)?; + + initial_diffs(&mut iss)?; + + if self.config.use_nsec3 { + incremental_nsec3(&mut iss)?; + } else { + incremental_nsec(&mut iss)?; } - SetCommands::NotifyCommand { args } => { - sc.notify_command = args; + + self.new_nsec_nsec3_sigs(&mut iss)?; + + if refresh_signatures { + self.refresh_some_signatures(&mut iss)?; + if self.state.key_roll.is_some() { + self.key_roll_signatures(&mut iss)?; + } } - SetCommands::FakeTime { opt_unixtime } => sc.faketime = opt_unixtime, + println!("incremental signing took {:?}", start.elapsed()); + + self.incremental_write_output(&iss)?; + Ok(()) } - *config_changed = true; - Ok(()) -} -fn next_owner_hash_to_name(next_owner_hash_hex: &str, apex: &StoredName) -> Result { - let mut builder = NameBuilder::new_bytes(); - builder - .append_chars(next_owner_hash_hex.chars()) - .map_err(|_| ())?; - let next_owner_name = builder.append_origin(apex).map_err(|_| ())?; - Ok(next_owner_name) -} + fn refresh_some_signatures(&mut self, iss: &mut IncrementalSigningState) -> Result<(), Error> { + let effective_lifetime = + self.config.signature_lifetime - self.config.minimal_remaining_validity; + let now = self.faketime_or_now(); + let now_system_time = UNIX_EPOCH + Duration::from(now.clone()); + let min_expire = now_system_time + self.config.minimal_remaining_validity; + let mut since_last_time: Duration = if now >= self.state.last_signature_refresh { + >::into(now.clone()) + - >::into(self.state.last_signature_refresh.clone()) + } else { + Duration::ZERO + }; -#[derive(Deserialize, Serialize)] -struct SignerConfig { - signer_state: PathBuf, - keyset_state: PathBuf, - zonefile_in: PathBuf, - zonefile_out: PathBuf, + // Limit to effective_lifetime in case of weird values. + if since_last_time > effective_lifetime { + since_last_time = effective_lifetime; + } + + let total_signatures = iss.rrsigs.len(); + + let to_sign = since_last_time.as_secs_f64() * (total_signatures as f64) + / effective_lifetime.as_secs_f64(); + let to_sign = to_sign.ceil() as usize; + + // Collect expiration times, owner names, and types to figure out what + // to sign. + let mut expire_sigs = vec![]; + for ((owner, rtype), r) in &iss.rrsigs { + let min_expiration = r + .iter() + .map(|r| { + let ZoneRecordData::Rrsig(rrsig) = r.data() else { + panic!("Rrsig expected"); + }; + rrsig.expiration().to_system_time(now_system_time) + }) + .min() + .expect("minimum should exist"); + let v = (min_expiration, owner, rtype); + expire_sigs.push(v); + } + + expire_sigs.sort(); + + let mut new_sigs = vec![]; + for (i, (expire, owner, rtype)) in expire_sigs.iter().enumerate() { + if *expire > min_expire && i >= to_sign { + break; + } + + let key = ((*owner).clone(), **rtype); + if **rtype == Rtype::NSEC3 { + let record = iss.nsec3s.get(&key.0).expect("NSEC3 record should exist"); + let records = [record.clone()]; + sign_records( + &records, + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + } else { + let records = iss.new_data.get(&key).expect("records should exist"); + sign_records( + records, + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + }; + } + + for (sigs, rtype) in new_sigs { + let key = (sigs[0].owner().clone(), rtype); + iss.rrsigs.insert(key, sigs); + } + + if to_sign != 0 { + // Only update last_signature_refresh when enough time has passed + // that at least one record got signed. + self.state.last_signature_refresh = now; + self.state_changed = true; + } + Ok(()) + } + + fn key_roll_signatures(&mut self, iss: &mut IncrementalSigningState) -> Result<(), Error> { + let key_roll_time = self.config.key_roll_time; + let key_roll_start = self.state.key_roll.as_ref().expect("should be there"); + + let now = self.faketime_or_now(); + + let since_start: Duration = >::into(now.clone()) + - >::into(key_roll_start.clone()); + + if since_start > key_roll_time { + // Full roll. Make sure all signatures are made using the new keys. + // Clear key_roll when we are done. + + let mut new_sigs = vec![]; + for ((owner, rtype), r) in &iss.rrsigs { + let key_tags: HashSet = r + .iter() + .map(|r| { + let ZoneRecordData::Rrsig(rrsig) = r.data() else { + panic!("Rrsig expected"); + }; + rrsig.key_tag() + }) + .collect(); + if key_tags == self.state.key_tags { + // Nothing to do. + continue; + } + + let key = ((*owner).clone(), *rtype); + if *rtype == Rtype::NSEC3 { + let record = iss.nsec3s.get(&key.0).expect("NSEC3 record should exist"); + let records = [record.clone()]; + sign_records( + &records, + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + } else { + let records = iss.new_data.get(&key).expect("records should exist"); + sign_records( + records, + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + }; + } + + for (sigs, rtype) in new_sigs { + let key = (sigs[0].owner().clone(), rtype); + iss.rrsigs.insert(key, sigs); + } + self.state.key_roll = None; + self.state_changed = true; + return Ok(()); + } + + let total_signatures = iss.rrsigs.len(); + + let to_sign = + since_start.as_secs_f64() * (total_signatures as f64) / key_roll_time.as_secs_f64(); + let to_sign = to_sign.ceil() as usize; + + // owner names, types, and key tags to figure out what to sign. + let mut sigs_key_tags = vec![]; + for ((owner, rtype), r) in &iss.rrsigs { + let key_tags: Vec = r + .iter() + .map(|r| { + let ZoneRecordData::Rrsig(rrsig) = r.data() else { + panic!("Rrsig expected"); + }; + rrsig.key_tag() + }) + .collect(); + let v = (owner, rtype, key_tags); + sigs_key_tags.push(v); + } + + sigs_key_tags.sort(); + + let mut new_sigs = vec![]; + for (i, (owner, rtype, key_tags)) in sigs_key_tags.iter().enumerate() { + if i >= to_sign { + break; + } + + if HashSet::::from_iter(key_tags.iter().copied()) == self.state.key_tags { + // Nothing to do. + continue; + } + + let key = ((*owner).clone(), **rtype); + if **rtype == Rtype::NSEC3 { + let record = iss.nsec3s.get(&key.0).expect("NSEC3 record should exist"); + let records = [record.clone()]; + sign_records( + &records, + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + } else { + let records = iss.new_data.get(&key).expect("records should exist"); + sign_records( + records, + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + }; + } + + for (sigs, rtype) in new_sigs { + let key = (sigs[0].owner().clone(), rtype); + iss.rrsigs.insert(key, sigs); + } + Ok(()) + } + + fn handle_keyset_changed(&mut self) -> bool { + if self.keyset_state_modified == self.state.keyset_state_modified { + // Nothing changed. + return false; + } + self.state.keyset_state_modified = self.keyset_state_modified.clone(); + self.state_changed = true; + + let mut apex_changed = false; + + // Check the APEX RRtypes that need to be removed. We + // should get that from keyset, but currently we don't. + // Just have a fixed list. + let apex_remove: HashSet = [Rtype::DNSKEY, Rtype::CDS, Rtype::CDNSKEY].into(); + + if apex_remove != self.state.apex_remove { + println!( + "APEX remove RRtypes changed: from {:?} to {apex_remove:?}", + self.state.apex_remove + ); + apex_changed = true; + self.state.apex_remove = apex_remove; + } + + // Check records that need to be added to the APEX. + let mut apex_extra = vec![]; + apex_extra.extend_from_slice(&self.keyset_state.dnskey_rrset); + apex_extra.extend_from_slice(&self.keyset_state.cds_rrset); + apex_extra.sort(); + + if apex_extra != self.state.apex_extra { + println!( + "APEX types changed: from {:?} to {apex_extra:?}", + self.state.apex_extra + ); + apex_changed = true; + self.state.apex_extra = apex_extra; + } + + // Check if a ZSK/CSK roll has started. + let mut key_tags = HashSet::new(); + for v in self.keyset_state.keyset.keys().values() { + let signer = match v.keytype() { + KeyType::Ksk(_) => false, + KeyType::Zsk(key_state) => key_state.signer(), + KeyType::Csk(_, key_state) => key_state.signer(), + KeyType::Include(_) => false, + }; + + if !signer { + continue; + } + + key_tags.insert(v.key_tag()); + } + + if key_tags != self.state.key_tags { + println!( + "key tags changed: from {:?} to {key_tags:?}", + self.state.key_tags + ); + self.state.key_roll = Some(self.faketime_or_now()); + self.state.key_tags = key_tags; + } + apex_changed + } + + fn resign(&mut self) -> Result<(), Error> { + self.sign_incrementally(true) + } + + fn faketime_or_now(&self) -> UnixTime { + self.config.faketime.clone().unwrap_or(UnixTime::now()) + } + + fn handle_nsec_nsec3(&self, iss: &mut IncrementalSigningState) { + // Note that we could try to regenerate the NSEC(3). Assume that + // switching between NSEC, NSEC3, and NSEC3 opt-out (or other NSEC3 + // parameter changes) is rare enough that we can just resign the full + // zone. + let key = (iss.origin.clone(), Rtype::NSEC3PARAM); + let opt_nsec3param = iss.old_data.get(&key); + if let Some(nsec3param_records) = opt_nsec3param { + // Zone was signed with NSEC3. + if !self.config.use_nsec3 { + // Zone is signed with NSEC3 but we want NSEC. + todo!(); + } + let ZoneRecordData::Nsec3param(nsec3param) = nsec3param_records[0].data() else { + panic!("ZoneRecordData::Nsec3param expected"); + }; + if nsec3param.hash_algorithm() != self.config.algorithm + || nsec3param.opt_out_flag() != self.config.opt_out + || nsec3param.iterations() != self.config.iterations + || *nsec3param.salt() != self.config.salt + { + // Parameters changed, resign. + todo!(); + } + + // Nothing has changed. Insert the old NSEC3PARAM records in the + // new zone data. + iss.new_data.insert(key, nsec3param_records.to_vec()); + } else { + // Zone was signed with NSEC, check if that is also the target. + if self.config.use_nsec3 { + // Resign the full zone with NSEC3. + todo!(); + } + + // Stay with NSEC. + } + } + + fn new_nsec_nsec3_sigs(&self, iss: &mut IncrementalSigningState) -> Result<(), Error> { + let mut new_sigs = vec![]; + if self.config.use_nsec3 { + for m in &iss.modified_nsecs { + let Some(nsec3) = iss.nsec3s.get(m) else { + panic!("NSEC3 for {m} should exist"); + }; + + let nsec3 = nsec3.clone(); + sign_records( + &[nsec3], + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + } + } else { + for m in &iss.modified_nsecs { + let Some(nsec) = iss.nsecs.get(m) else { + panic!("NSEC for {m} should exist"); + }; + + let nsec = nsec.clone(); + sign_records( + &[nsec], + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + } + } + for (sig, rtype) in new_sigs { + let key = (sig[0].owner().clone(), rtype); + iss.rrsigs.insert(key, sig); + } + Ok(()) + } + + fn incremental_write_output(&self, iss: &IncrementalSigningState) -> Result<(), Error> { + let start = Instant::now(); + let mut writer = { + let filename = &self.config.zonefile_out; + let file = File::create(filename) + .map_err(|e| format!("unable to create file {}: {e}", filename.display()))?; + BufWriter::new(file) + // FileOrStdout::File(file) + }; + + for data in iss.new_data.values() { + for rr in data { + writer + .write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))) + .map_err(|e| format!("unable write signed zone: {e}"))?; + } + } + for rr in iss.nsecs.values() { + writer + .write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))) + .map_err(|e| format!("unable write signed zone: {e}"))?; + } + for rr in iss.nsec3s.values() { + writer + .write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))) + .map_err(|e| format!("unable write signed zone: {e}"))?; + } + for data in iss.rrsigs.values() { + for rr in data { + let ZoneRecordData::Rrsig(rrsig) = rr.data() else { + panic!("RRSIG expected"); + }; + let rr = Record::new(rr.owner(), rr.class(), rr.ttl(), YyyyMmDdHhMMSsRrsig(rrsig)); + writer + .write_fmt(format_args!("{}\n", rr.display_zonefile(DISPLAY_KIND))) + .map_err(|e| format!("unable write signed zone: {e}"))?; + } + } + println!("writing output took {:?}", start.elapsed()); + Ok(()) + } + + fn load_apex_records(&self, iss: &mut IncrementalSigningState) -> Result<(), Error> { + // Assume that the APEX records have been copied from KeySetState to + // SignerState. Now update the APEX in new_data. + + // Delete all types in apex_remove. + for t in &self.state.apex_remove { + let key = (iss.origin.clone(), *t); + iss.new_data.remove(&key); + iss.rrsigs.remove(&key); + } + + for r in &self.state.apex_extra { + let zonefile = + domain::zonefile::inplace::Zonefile::from((r.to_string() + "\n").as_ref() as &str); + for entry in zonefile { + let entry = entry.map_err::(|e| format!("bad entry: {e}\n").into())?; + + // We only care about records in a zonefile + let Entry::Record(record) = entry else { + continue; + }; + + let owner = record.owner().to_name::(); + let data = record.data().clone().try_flatten_into().unwrap(); + let r = Record::new(owner.clone(), record.class(), record.ttl(), data); + + if r.rtype() == Rtype::RRSIG { + let ZoneRecordData::Rrsig(rrsig) = r.data() else { + panic!("RRSIG expected"); + }; + let key = (owner, rrsig.type_covered()); + let mut records = vec![r]; + if let Some(v) = iss.rrsigs.get_mut(&key) { + v.append(&mut records); + } else { + iss.rrsigs.insert(key, records); + } + } else { + let key = (owner, r.rtype()); + let mut records = vec![r]; + if let Some(v) = iss.new_data.get_mut(&key) { + v.append(&mut records); + } else { + iss.new_data.insert(key, records); + } + } + } + } + Ok(()) + } +} + +fn next_owner_hash_to_name(next_owner_hash_hex: &str, apex: &StoredName) -> Result { + let mut builder = NameBuilder::new_bytes(); + builder + .append_chars(next_owner_hash_hex.chars()) + .map_err(|_| ())?; + let next_owner_name = builder.append_origin(apex).map_err(|_| ())?; + Ok(next_owner_name) +} + +#[derive(Deserialize, Serialize)] +struct SignerConfig { + signer_state: PathBuf, + keyset_state: PathBuf, + zonefile_in: PathBuf, + zonefile_out: PathBuf, inception_offset: Duration, signature_lifetime: Duration, @@ -1791,6 +2136,12 @@ struct SignerConfig { serial_policy: SerialPolicy, notify_command: Vec, + /// Minimum period for updating signatures. + signature_refresh_interval: Duration, + + /// Maxmimum time to resign all records with new ZSKs or CSKs. + key_roll_time: Duration, + /// Fake time to use when signing. /// /// This is need for integration tests. @@ -1804,6 +2155,25 @@ struct SignerState { zonefile_modified: UnixTime, minimum_expiration: UnixTime, previous_serial: Option, + + /// APEX RRtypes to remove. Should come from keyset, currently hardcoded. + #[serde(default)] + apex_remove: HashSet, + + /// extra APEX records, from keyset. + #[serde(default)] + apex_extra: Vec, + + /// Current CSK/ZSK key tags. + #[serde(default)] + key_tags: HashSet, + + /// Start time of CSK/KSK key roll. + #[serde(default)] + key_roll: Option, + + /// Last time some signature were refreshed. + last_signature_refresh: UnixTime, } type RtypeSet = HashSet; @@ -1828,20 +2198,77 @@ struct IncrementalSigningState { } impl IncrementalSigningState { - fn new(origin: Name, sc: &SignerConfig) -> Self { - let now = - Into::::into(sc.faketime.clone().unwrap_or(UnixTime::now())).as_secs() as u32; - let inception = (now - sc.inception_offset.as_secs() as u32).into(); - let expiration = (now + sc.signature_lifetime.as_secs() as u32).into(); + fn new(ws: &WorkSpace) -> Result { + let origin = ws.keyset_state.keyset.name(); + let origin = Name::::octets_from(origin.clone()); + + let mut keys = Vec::new(); + for (k, v) in ws.keyset_state.keyset.keys() { + let signer = match v.keytype() { + KeyType::Ksk(_) => false, + KeyType::Zsk(key_state) => key_state.signer(), + KeyType::Csk(_, key_state) => key_state.signer(), + KeyType::Include(_) => false, + }; + + if signer { + let privref = v.privref().ok_or("missing private key")?; + let priv_url = Url::parse(privref).expect("valid URL expected"); + let private_data = if priv_url.scheme() == "file" { + std::fs::read_to_string(priv_url.path()).map_err::(|e| { + format!("unable read from file {}: {e}", priv_url.path()).into() + })? + } else { + panic!("unsupported URL scheme in {priv_url}"); + }; + let secret_key = SecretKeyBytes::parse_from_bind(&private_data) + .map_err::(|e| { + format!("unable to parse private key file {privref}: {e}").into() + })?; + let pub_url = Url::parse(k).expect("valid URL expected"); + let public_data = if pub_url.scheme() == "file" { + std::fs::read_to_string(pub_url.path()).map_err::(|e| { + format!("unable read from file {}: {e}", pub_url.path()).into() + })? + } else { + panic!("unsupported URL scheme in {pub_url}"); + }; + let public_key = + parse_from_bind::(&public_data).map_err::(|e| { + format!("unable to parse public key file {k}: {e}").into() + })?; + + let key_pair = KeyPair::from_bytes(&secret_key, public_key.data()) + .map_err::(|e| { + format!("private key {privref} and public key {k} do not match: {e}").into() + })?; + let signing_key = SigningKey::new( + public_key.owner().clone(), + public_key.data().flags(), + key_pair, + ); + keys.push(signing_key); + } + } + + let now = ws.faketime_or_now(); + let now_u32 = Into::::into(now.clone()).as_secs() as u32; + let inception = (now_u32 - ws.config.inception_offset.as_secs() as u32).into(); + let expiration = (now_u32 + ws.config.signature_lifetime.as_secs() as u32).into(); // This is the only way to deal with opt-out. There is no data type // for flags or constant for opt-out. Creating an Nsec3param makes it // possible to set opt-out. - let mut nsec3param = Nsec3param::new(sc.algorithm, 0, sc.iterations, sc.salt.clone()); - if sc.opt_out { + let mut nsec3param = Nsec3param::new( + ws.config.algorithm, + 0, + ws.config.iterations, + ws.config.salt.clone(), + ); + if ws.config.opt_out { nsec3param.set_opt_out_flag(); } - Self { + Ok(Self { origin, old_data: HashMap::new(), new_data: BTreeMap::new(), @@ -1850,11 +2277,11 @@ impl IncrementalSigningState { rrsigs: HashMap::new(), changes: HashMap::new(), modified_nsecs: HashSet::new(), - keys: vec![], + keys, inception, expiration, nsec3param, - } + }) } } @@ -2006,9 +2433,10 @@ fn load_unsigned_zone(iss: &mut IncrementalSigningState, path: &PathBuf) -> Resu let record: StoredRecord = record.flatten_into(); match record.data() { - ZoneRecordData::Rrsig(_) => (), // Ignore. - ZoneRecordData::Nsec(_) => (), // Ignore. - ZoneRecordData::Nsec3(_) => todo!(), + ZoneRecordData::Rrsig(_) + | ZoneRecordData::Nsec(_) + | ZoneRecordData::Nsec3(_) + | ZoneRecordData::Nsec3param(_) => (), // Ignore. _ => { if records.is_empty() { records.push(record); @@ -2050,79 +2478,12 @@ fn load_unsigned_zone(iss: &mut IncrementalSigningState, path: &PathBuf) -> Resu Ok(()) } -fn load_apex_records(kss: &KeySetState, iss: &mut IncrementalSigningState) -> Result<(), Error> { - let mut records = vec![]; - let mut rrsig_records = vec![]; - for r in &kss.dnskey_rrset { - let zonefile = - domain::zonefile::inplace::Zonefile::from((r.to_string() + "\n").as_ref() as &str); - for entry in zonefile { - let entry = entry.map_err::(|e| format!("bad entry: {e}\n").into())?; - - // We only care about records in a zonefile - let Entry::Record(record) = entry else { - continue; - }; +fn load_signed_only(iss: &mut IncrementalSigningState) { + // Copy old data to new data. - let owner = record.owner().to_name::(); - let data = record.data().clone().try_flatten_into().unwrap(); - let r = Record::new(owner, record.class(), record.ttl(), data); - - if r.rtype() == Rtype::RRSIG { - rrsig_records.push(r); - } else { - records.push(r); - } - } + for (k, v) in &iss.old_data { + iss.new_data.insert(k.clone(), v.clone()); } - - if !records.is_empty() { - let key = (records[0].owner().clone(), Rtype::DNSKEY); - iss.new_data.insert(key, records); - } - if !rrsig_records.is_empty() { - let key = (rrsig_records[0].owner().clone(), Rtype::DNSKEY); - iss.rrsigs.insert(key, rrsig_records); - } - - for r in &kss.cds_rrset { - let zonefile = - domain::zonefile::inplace::Zonefile::from((r.to_string() + "\n").as_ref() as &str); - for entry in zonefile { - let entry = entry.map_err::(|e| format!("bad entry: {e}\n").into())?; - - // We only care about records in a zonefile - let Entry::Record(record) = entry else { - continue; - }; - - let owner = record.owner().to_name::(); - let data = record.data().clone().try_flatten_into().unwrap(); - let r = Record::new(owner.clone(), record.class(), record.ttl(), data); - - if r.rtype() == Rtype::RRSIG { - let ZoneRecordData::Rrsig(rrsig) = r.data() else { - panic!("RRSIG expected"); - }; - let key = (owner, rrsig.type_covered()); - records = vec![r]; - if let Some(v) = iss.rrsigs.get_mut(&key) { - v.append(&mut records); - } else { - iss.rrsigs.insert(key, records); - } - } else { - let key = (owner, r.rtype()); - records = vec![r]; - if let Some(v) = iss.new_data.get_mut(&key) { - v.append(&mut records); - } else { - iss.new_data.insert(key, records); - } - } - } - } - Ok(()) } fn initial_diffs(iss: &mut IncrementalSigningState) -> Result<(), Error> { From add6ac884a77aee7126038c8480f13becaac4116 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Mon, 16 Feb 2026 14:17:37 +0100 Subject: [PATCH 12/20] Add ZONEMD to incremental signing. And some small changes. --- src/commands/signer.rs | 290 +++++++++++++++++++++++++++-------------- 1 file changed, 189 insertions(+), 101 deletions(-) diff --git a/src/commands/signer.rs b/src/commands/signer.rs index 86a69b6c..23a09083 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -104,10 +104,6 @@ enum Commands { zonefile_out: Option, }, Sign { - /// Hash only, don't sign. - #[arg(long, action)] - hash_only: bool, - /// Use layout in signed zone and print comments on DNSSEC records #[arg(long, action)] extra_comments: bool, @@ -157,6 +153,15 @@ enum SetCommands { #[arg(value_parser = super::keyset::cmd::parse_duration)] duration: Duration, }, + /// Set how much time the signatures still have to be valid. + /// + /// New signatures will be generated before the time until the expiration + /// time is less than that. + RemainTime { + /// The required remaining time. + #[arg(value_parser = super::keyset::cmd::parse_duration)] + duration: Duration, + }, UseNsec3 { #[arg(action = clap::ArgAction::Set)] boolean: bool, @@ -210,7 +215,6 @@ enum SetCommands { #[derive(Default)] struct SigningOptions { - hash_only: bool, extra_comments: bool, preceed_zone_with_hash_list: bool, order_nsec3_rrs_by_unhashed_owner_name: bool, @@ -360,7 +364,7 @@ impl Signer { keyset_state, inception_offset: Duration::from_secs(ONE_DAY), signature_lifetime: Duration::from_secs(FOUR_WEEKS), - minimal_remaining_validity: Duration::from_secs(TWO_WEEKS), + remain_time: Duration::from_secs(TWO_WEEKS), use_nsec3: false, algorithm: Nsec3HashAlgorithm::SHA1, iterations: 0, @@ -450,7 +454,6 @@ impl Signer { match self.cmd { Commands::Create { .. } => unreachable!(), Commands::Sign { - hash_only, extra_comments, preceed_zone_with_hash_list, order_nsec3_rrs_by_unhashed_owner_name, @@ -458,7 +461,6 @@ impl Signer { use_yyyymmddhhmmss_rrsig_format, } => { let options = SigningOptions { - hash_only, extra_comments, preceed_zone_with_hash_list, order_nsec3_rrs_by_unhashed_owner_name, @@ -514,12 +516,6 @@ impl Signer { env: impl Env, options: SigningOptions, ) -> Result<(), Error> { - let signing_mode = if options.hash_only { - SigningMode::HashOnly - } else { - SigningMode::HashAndSign - }; - // Populate the signer state fields from keyset state. ws.handle_keyset_changed(); @@ -739,29 +735,6 @@ impl Signer { // Find the apex. let (apex, zone_class, ttl, soa_serial) = Self::find_apex(&records).unwrap(); - // The original ldns-signzone filters out (with warnings) NSEC3 RRs, - // or RRSIG RRs covering NSEC3 RRs, where the hashed owner name - // doesn't correspond to an unhashed owner name in the zone. To work - // this out you have to NSEC3 hash every owner name during loading and - // filter out any NSEC3 hashed owner name that doesn't appear in the - // built NSEC3 hash set. To generate the NSEC3 hashes we have to know - // the settings that were used to NSEC3 hash the zone, i.e. we have to - // find an NSEC3PARAM RR at the apex, or an NSEC3 RR in the zone. But - // we don't know what the apex is until we find the SOA, and checking - // DNSKEYs and loading key files is quick so we do that first. Then - // once we get here we have the ordered zone, we know the apex, and we - // can find the NSEC3PARAM RR. Then we can generate NSEC3 hashes for - // owner names. - // - // However, WE DON'T DO THIS as it was (a) discovered that - // ldns-signzone is too simplistic in its approach as it would wrongly - // conclude that NSEC3 hashes for empty non-terminals lack a matching - // owner name in the zone because it only determined ENTs _after_ - // ignoring and warning about hashed owner names that don't correspond - // to an unhashed owner name in the zone, and (b) that it would be - // better for ldns-signzone to strip out NSEC(3)s on loading anyway as - // it should only operate on unsigned zone input. - if !zonemd.is_empty() { Self::replace_apex_zonemd_with_placeholder( &mut records, @@ -880,40 +853,24 @@ impl Signer { // Set last_signature_refresh to the current time. ws.state.last_signature_refresh = now; - let signing_config: SigningConfig<_, _> = match signing_mode { - SigningMode::HashOnly | SigningMode::HashAndSign => { - // LDNS doesn't add NSECs to a zone that already has NSECs or - // NSEC3s. It *does* add NSEC3 if the zone has NSECs. As noted in - // load_zone() we instead, as LDNS should, strip NSEC(3)s on load - // and thus always add NSEC(3)s when hashing. - // - // Note: Assuming that we want to later be able to support - // transition between NSEC <-> NSEC3 we will need to be able to - // sign with more than one hashing configuration at once. - if ws.config.use_nsec3 { - let params = Nsec3param::new( - ws.config.algorithm, - 0, - ws.config.iterations, - ws.config.salt.clone(), - ); - let mut nsec3_config = GenerateNsec3Config::new(params); - if ws.config.opt_out { - nsec3_config = nsec3_config.with_opt_out(); - } - SigningConfig::new(DenialConfig::Nsec3(nsec3_config), inception, expiration) - } else { - SigningConfig::new( - DenialConfig::Nsec(GenerateNsecConfig::new()), - inception, - expiration, - ) - } - } /* - SigningMode::None => { - SigningConfig::new(DenialConfig::AlreadyPresent, inception, expiration) - } - */ + let signing_config = if ws.config.use_nsec3 { + let params = Nsec3param::new( + ws.config.algorithm, + 0, + ws.config.iterations, + ws.config.salt.clone(), + ); + let mut nsec3_config = GenerateNsec3Config::new(params); + if ws.config.opt_out { + nsec3_config = nsec3_config.with_opt_out(); + } + SigningConfig::new(DenialConfig::Nsec3(nsec3_config), inception, expiration) + } else { + SigningConfig::new( + DenialConfig::Nsec(GenerateNsecConfig::new()), + inception, + expiration, + ) }; records @@ -933,17 +890,15 @@ impl Signer { let _ = records.insert(zrr); } - if signing_mode == SigningMode::HashAndSign { - Self::update_zonemd_rrsig( - &apex, - &mut records, - &signing_keys, - &zonemd_rrs, - inception, - expiration, - ) - .map_err(|err| format!("ZONEMD re-signing error: {err}"))?; - } + Self::update_zonemd_rrsig( + &apex, + &mut records, + &signing_keys, + &zonemd_rrs, + inception, + expiration, + ) + .map_err(|err| format!("ZONEMD re-signing error: {err}"))?; } let now_ts = Timestamp::now(); @@ -1541,6 +1496,9 @@ impl WorkSpace { SetCommands::Lifetime { duration } => { self.config.signature_lifetime = duration; } + SetCommands::RemainTime { duration } => { + self.config.remain_time = duration; + } SetCommands::UseNsec3 { boolean } => { self.config.use_nsec3 = boolean; } @@ -1635,6 +1593,12 @@ impl WorkSpace { self.new_nsec_nsec3_sigs(&mut iss)?; + if !self.config.zonemd.is_empty() { + let start = Instant::now(); + self.add_zonemd(&mut iss)?; + println!("ZONEMD took {:?}", start.elapsed()); + } + if refresh_signatures { self.refresh_some_signatures(&mut iss)?; if self.state.key_roll.is_some() { @@ -1648,11 +1612,10 @@ impl WorkSpace { } fn refresh_some_signatures(&mut self, iss: &mut IncrementalSigningState) -> Result<(), Error> { - let effective_lifetime = - self.config.signature_lifetime - self.config.minimal_remaining_validity; + let effective_lifetime = self.config.signature_lifetime - self.config.remain_time; let now = self.faketime_or_now(); let now_system_time = UNIX_EPOCH + Duration::from(now.clone()); - let min_expire = now_system_time + self.config.minimal_remaining_validity; + let min_expire = now_system_time + self.config.remain_time; let mut since_last_time: Duration = if now >= self.state.last_signature_refresh { >::into(now.clone()) - >::into(self.state.last_signature_refresh.clone()) @@ -1698,7 +1661,17 @@ impl WorkSpace { } let key = ((*owner).clone(), **rtype); - if **rtype == Rtype::NSEC3 { + if **rtype == Rtype::NSEC { + let record = iss.nsecs.get(&key.0).expect("NSEC record should exist"); + let records = [record.clone()]; + sign_records( + &records, + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + } else if **rtype == Rtype::NSEC3 { let record = iss.nsec3s.get(&key.0).expect("NSEC3 record should exist"); let records = [record.clone()]; sign_records( @@ -2104,6 +2077,131 @@ impl WorkSpace { } } } + + if !self.config.zonemd.is_empty() { + let zonemd = Zonemd::new( + 0.into(), + ZonemdScheme::SIMPLE, + ZonemdAlgorithm::SHA384, + Bytes::new(), + ); + let record = Record::new( + iss.origin.clone(), + Class::IN, + Ttl::ZERO, + ZoneRecordData::Zonemd(zonemd), + ); + let records = vec![record]; + let key = (iss.origin.clone(), Rtype::ZONEMD); + iss.new_data.insert(key, records); + } + Ok(()) + } + + fn add_zonemd(&self, iss: &mut IncrementalSigningState) -> Result<(), Error> { + // Get the SOA record. We need that for the Serial and for the + // TTL. + let key = (iss.origin.clone(), Rtype::SOA); + let soa_records = iss + .new_data + .get(&key) + .expect("SOA record should be present"); + let ZoneRecordData::Soa(soa) = soa_records[0].data() else { + panic!("SOA record expected"); + }; + + let start = Instant::now(); + + // Create a Vec with all records to be able to sort them in canonical + // order. Ignore ZONEMD and RRSIGs of ZONEMD records. + let mut all = vec![]; + + let mut data: Vec<_> = iss + .new_data + .iter() + .filter_map(|((o, t), r)| { + if *o != iss.origin || *t != Rtype::ZONEMD { + Some(r) + } else { + None + } + }) + .flatten() + .collect(); + all.append(&mut data); + + let mut data: Vec<_> = iss.nsecs.values().collect(); + all.append(&mut data); + + let mut data: Vec<_> = iss.nsec3s.values().collect(); + all.append(&mut data); + + let mut data: Vec<_> = iss + .rrsigs + .iter() + .filter_map(|((o, t), r)| { + if *o != iss.origin || *t != Rtype::ZONEMD { + Some(r) + } else { + None + } + }) + .flatten() + .collect(); + all.append(&mut data); + + //all.sort_by(|e1, e2| CanonicalOrd::canonical_cmp(*e1, *e2)); + all.par_sort_by(|e1, e2| CanonicalOrd::canonical_cmp(*e1, *e2)); + + println!("ZONEMD prepare and sort took {:?}", start.elapsed()); + + let start = Instant::now(); + + let mut zonemd_records = vec![]; + for z in &self.config.zonemd { + if z.0 != ZonemdScheme::SIMPLE { + return Err("unsupported zonemd scheme (only SIMPLE is supported)".into()); + } + let mut buf: Vec = Vec::new(); + let mut ctx = match z.1 { + ZonemdAlgorithm::SHA384 => digest::Context::new(&digest::SHA384), + ZonemdAlgorithm::SHA512 => digest::Context::new(&digest::SHA512), + _ => unreachable!(), + }; + for r in &all { + buf.clear(); + with_infallible(|| r.compose_canonical(&mut buf)); + ctx.update(&buf); + } + let digest = ctx.finish(); + let zonemd = Zonemd::new( + soa.serial(), + z.0, + z.1, + Bytes::copy_from_slice(digest.as_ref()), + ); + let record = Record::new( + iss.origin.clone(), + soa_records[0].class(), + soa_records[0].ttl(), + ZoneRecordData::Zonemd(zonemd), + ); + zonemd_records.push(record); + } + + println!("ZONEMD hash took {:?}", start.elapsed()); + + let key = (iss.origin.clone(), Rtype::ZONEMD); + let mut new_sigs = vec![]; + sign_records( + &zonemd_records, + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + iss.new_data.insert(key.clone(), zonemd_records); + iss.rrsigs.insert(key, new_sigs[0].0.clone()); Ok(()) } } @@ -2126,7 +2224,7 @@ struct SignerConfig { inception_offset: Duration, signature_lifetime: Duration, - minimal_remaining_validity: Duration, + remain_time: Duration, use_nsec3: bool, algorithm: Nsec3HashAlgorithm, iterations: u16, @@ -2328,7 +2426,8 @@ fn load_signed_zone(iss: &mut IncrementalSigningState, path: &PathBuf) -> Result if record.owner() == rrsig_records[0].owner() && rrsig.type_covered() == type_covered { - todo!(); + rrsig_records.push(record); + continue; } let key = (rrsig_records[0].owner().clone(), type_covered); @@ -2436,7 +2535,8 @@ fn load_unsigned_zone(iss: &mut IncrementalSigningState, path: &PathBuf) -> Resu ZoneRecordData::Rrsig(_) | ZoneRecordData::Nsec(_) | ZoneRecordData::Nsec3(_) - | ZoneRecordData::Nsec3param(_) => (), // Ignore. + | ZoneRecordData::Nsec3param(_) + | ZoneRecordData::Zonemd(_) => (), // Ignore. _ => { if records.is_empty() { records.push(record); @@ -3715,18 +3815,6 @@ fn sign_records( Ok(()) } -//------------ SigningMode --------------------------------------------------- - -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -enum SigningMode { - /// Both hash (NSEC/NSEC3) and sign zone records. - #[default] - HashAndSign, - - /// Only hash (NSEC/NSEC3) zone records, don't sign them. - HashOnly, -} - //------------ ZonemdTuple --------------------------------------------------- #[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)] From 9f4c9da5b5d7f578132a260a5b711d25f1763e26 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Mon, 23 Feb 2026 11:59:28 +0100 Subject: [PATCH 13/20] Support for moving between NSEC and NSEC3. --- src/commands/signer.rs | 152 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 137 insertions(+), 15 deletions(-) diff --git a/src/commands/signer.rs b/src/commands/signer.rs index 23a09083..50426b50 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -22,12 +22,14 @@ use domain::crypto::sign::{KeyPair, SecretKeyBytes}; use domain::dep::octseq::OctetsFrom; use domain::dnssec::common::{nsec3_hash, parse_from_bind}; use domain::dnssec::sign::denial::config::DenialConfig; -use domain::dnssec::sign::denial::nsec::GenerateNsecConfig; -use domain::dnssec::sign::denial::nsec3::{mk_hashed_nsec3_owner_name, GenerateNsec3Config}; +use domain::dnssec::sign::denial::nsec::{generate_nsecs, GenerateNsecConfig}; +use domain::dnssec::sign::denial::nsec3::{ + generate_nsec3s, mk_hashed_nsec3_owner_name, GenerateNsec3Config, Nsec3ParamTtlMode, +}; use domain::dnssec::sign::error::SigningError; use domain::dnssec::sign::keys::keyset::{KeyType, UnixTime}; use domain::dnssec::sign::keys::SigningKey; -use domain::dnssec::sign::records::{OwnerRrs, RecordsIter, Rrset, SortedRecords}; +use domain::dnssec::sign::records::{DefaultSorter, OwnerRrs, RecordsIter, Rrset, SortedRecords}; use domain::dnssec::sign::signatures::rrsigs::sign_rrset; use domain::dnssec::sign::traits::{Signable, SignableZoneInPlace}; use domain::dnssec::sign::SigningConfig; @@ -1569,7 +1571,7 @@ impl WorkSpace { load_signed_zone(&mut iss, &self.config.zonefile_out).unwrap(); println!("loading signed zone took {:?}", start.elapsed()); - self.handle_nsec_nsec3(&mut iss); + self.handle_nsec_nsec3(&mut iss)?; if load_unsigned { let start = Instant::now(); @@ -1908,7 +1910,7 @@ impl WorkSpace { self.config.faketime.clone().unwrap_or(UnixTime::now()) } - fn handle_nsec_nsec3(&self, iss: &mut IncrementalSigningState) { + fn handle_nsec_nsec3(&self, iss: &mut IncrementalSigningState) -> Result<(), Error> { // Note that we could try to regenerate the NSEC(3). Assume that // switching between NSEC, NSEC3, and NSEC3 opt-out (or other NSEC3 // parameter changes) is rare enough that we can just resign the full @@ -1919,18 +1921,22 @@ impl WorkSpace { // Zone was signed with NSEC3. if !self.config.use_nsec3 { // Zone is signed with NSEC3 but we want NSEC. - todo!(); + let start = Instant::now(); + remove_nsec_nsec3(iss); + new_nsec_chain(iss)?; + println!("replacing NSEC3 with NSEC took {:?}", start.elapsed()); + return Ok(()); } let ZoneRecordData::Nsec3param(nsec3param) = nsec3param_records[0].data() else { panic!("ZoneRecordData::Nsec3param expected"); }; - if nsec3param.hash_algorithm() != self.config.algorithm - || nsec3param.opt_out_flag() != self.config.opt_out - || nsec3param.iterations() != self.config.iterations - || *nsec3param.salt() != self.config.salt - { + if *nsec3param != iss.nsec3param { // Parameters changed, resign. - todo!(); + let start = Instant::now(); + remove_nsec_nsec3(iss); + new_nsec3_chain(iss)?; + println!("updating NSEC3 parameters took {:?}", start.elapsed()); + return Ok(()); } // Nothing has changed. Insert the old NSEC3PARAM records in the @@ -1940,11 +1946,15 @@ impl WorkSpace { // Zone was signed with NSEC, check if that is also the target. if self.config.use_nsec3 { // Resign the full zone with NSEC3. - todo!(); + let start = Instant::now(); + remove_nsec_nsec3(iss); + new_nsec3_chain(iss)?; + println!("replacing NSEC with NSEC3 took {:?}", start.elapsed()); + return Ok(()); } - // Stay with NSEC. } + Ok(()) } fn new_nsec_nsec3_sigs(&self, iss: &mut IncrementalSigningState) -> Result<(), Error> { @@ -2206,6 +2216,118 @@ impl WorkSpace { } } +fn remove_nsec_nsec3(iss: &mut IncrementalSigningState) { + for k in iss.nsecs.keys() { + let key = (k.clone(), Rtype::NSEC); + iss.rrsigs.remove(&key); + } + iss.nsecs = BTreeMap::new(); + + for k in iss.nsec3s.keys() { + let key = (k.clone(), Rtype::NSEC3); + iss.rrsigs.remove(&key); + } + iss.nsec3s = BTreeMap::new(); +} + +fn new_nsec_chain(iss: &mut IncrementalSigningState) -> Result<(), Error> { + let records = get_unsigned_sorted(iss); + let records_iter = RecordsIter::new_from_refs(&records); + let config = GenerateNsecConfig::new(); + let nsec_records = generate_nsecs(&iss.origin, records_iter, &config) + .map_err(|e| format!("generate_nsec3s failed: {e}"))?; + + // Collect signatures here. + let mut new_sigs = vec![]; + + for r in nsec_records { + let record = Record::new( + r.owner().clone(), + r.class(), + r.ttl(), + ZoneRecordData::Nsec(r.data().clone()), + ); + iss.nsecs.insert(record.owner().clone(), record.clone()); + sign_records( + &[record], + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + } + for (sig, rtype) in new_sigs { + let key = (sig[0].owner().clone(), rtype); + iss.rrsigs.insert(key, sig); + } + Ok(()) +} + +fn new_nsec3_chain(iss: &mut IncrementalSigningState) -> Result<(), Error> { + let records = get_unsigned_sorted(iss); + let records_iter = RecordsIter::new_from_refs(&records); + let config = GenerateNsec3Config::<_, DefaultSorter>::new(iss.nsec3param.clone()) + .with_ttl_mode(Nsec3ParamTtlMode::SoaMinimum); + let nsec3_records = generate_nsec3s(&iss.origin, records_iter, &config) + .map_err(|e| format!("generate_nsec3s failed: {e}"))?; + + // Collect signatures here. + let mut new_sigs = vec![]; + + let r = nsec3_records.nsec3param; + let record = Record::new( + r.owner().clone(), + r.class(), + r.ttl(), + ZoneRecordData::Nsec3param(r.data().clone()), + ); + let key = (record.owner().clone(), Rtype::NSEC3PARAM); + let records = vec![record.clone()]; + + // Insert in both old and new data. + sign_records( + &[record], + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + iss.old_data.insert(key.clone(), records.clone()); + iss.new_data.insert(key, records); + + for r in nsec3_records.nsec3s { + let record = Record::new( + r.owner().clone(), + r.class(), + r.ttl(), + ZoneRecordData::Nsec3(r.data().clone()), + ); + iss.nsec3s.insert(record.owner().clone(), record.clone()); + sign_records( + &[record], + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + } + for (sig, rtype) in new_sigs { + let key = (sig[0].owner().clone(), rtype); + iss.rrsigs.insert(key, sig); + } + Ok(()) +} + +fn get_unsigned_sorted(iss: &IncrementalSigningState) -> Vec<&Zrd> { + // Create a Vec with all unsigned records to be able to sort them in + // canonical order. + + let mut data: Vec<_> = iss.old_data.values().flatten().collect(); + data.par_sort_by(|e1, e2| CanonicalOrd::canonical_cmp(*e1, *e2)); + + data +} + fn next_owner_hash_to_name(next_owner_hash_hex: &str, apex: &StoredName) -> Result { let mut builder = NameBuilder::new_bytes(); builder @@ -3798,7 +3920,7 @@ fn sign_records( return Ok(()); } - let rrset = Rrset::new(records).map_err(|e| format!("Rrset::new failed: {e}"))?; + let rrset = Rrset::new_from_owned(records).map_err(|e| format!("Rrset::new failed: {e}"))?; let mut rrsig_records = vec![]; for key in keys { let rrsig = sign_rrset(key, &rrset, inception, expiration) From 4984f1fe82d828b18064157c89d2a1d28dd25579 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Mon, 23 Feb 2026 17:37:23 +0100 Subject: [PATCH 14/20] Add serial policy to incremental signing. --- src/commands/signer.rs | 242 ++++++++++++++++++++++++----------------- 1 file changed, 142 insertions(+), 100 deletions(-) diff --git a/src/commands/signer.rs b/src/commands/signer.rs index 50426b50..50949d1a 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -633,106 +633,10 @@ impl Signer { // Make sure, zonemd arguments are unique let zonemd: HashSet = HashSet::from_iter(ws.config.zonemd.clone()); - // implement SOA serial policies. There are four policies: - // 1) Keep. Copy the serial from the unsigned zone. Refuse to sign - // if the serial did not change. - // 2) Increment. Copy the serial from the unsigned zone but increment - // the serial if the zone needs to be signed an the serial in - // the unsigned zone did not change. - // 3) Unix timestamp. The current time in Unix seconds. Increment if - // that does not result in a higher serial. - // 4) Broken down time (YYYYMMDDnn). The current day plus a serial - // number. Implies increment to generate different serial numbers - // over a day. - // SAFETY: Already checked before this point. - let zone_soa_rr = records.find_soa().unwrap(); - let ZoneRecordData::Soa(zone_soa) = zone_soa_rr.first().data() else { - unreachable!(); - }; - - match ws.config.serial_policy { - SerialPolicy::Keep => { - if let Some(previous_serial) = ws.state.previous_serial { - if zone_soa.serial() <= previous_serial { - return Err( - "Serial policy is Keep but upstream serial did not increase".into() - ); - } - } - - ws.state.previous_serial = Some(zone_soa.serial()); - } - SerialPolicy::Increment => { - let mut serial = zone_soa.serial(); - if let Some(previous_serial) = ws.state.previous_serial { - if serial <= previous_serial { - serial = previous_serial.add(1); - - let new_soa = ZoneRecordData::Soa(Soa::new( - zone_soa.mname().clone(), - zone_soa.rname().clone(), - serial, - zone_soa.refresh(), - zone_soa.retry(), - zone_soa.expire(), - zone_soa.minimum(), - )); - - records.update_data(|rr| rr.rtype() == Rtype::SOA, new_soa); - } - } - ws.state.previous_serial = Some(serial); - } - SerialPolicy::UnixSeconds => { - let mut serial = Serial::now(); - if let Some(previous_serial) = ws.state.previous_serial { - if serial <= previous_serial { - serial = previous_serial.add(1); - } - } - - let new_soa = ZoneRecordData::Soa(Soa::new( - zone_soa.mname().clone(), - zone_soa.rname().clone(), - serial, - zone_soa.refresh(), - zone_soa.retry(), - zone_soa.expire(), - zone_soa.minimum(), - )); - - records.update_data(|rr| rr.rtype() == Rtype::SOA, new_soa); - ws.state.previous_serial = Some(serial); - } - SerialPolicy::Date => { - let ts = JiffTimestamp::now(); - let zone = Zoned::new(ts, TimeZone::UTC); - let serial = ((zone.year() as u32 * 100 + zone.month() as u32) * 100 - + zone.day() as u32) - * 100; - let mut serial: Serial = serial.into(); - - if let Some(previous_serial) = ws.state.previous_serial { - if serial <= previous_serial { - serial = previous_serial.add(1); - } - } - - let new_soa = ZoneRecordData::Soa(Soa::new( - zone_soa.mname().clone(), - zone_soa.rname().clone(), - serial, - zone_soa.refresh(), - zone_soa.retry(), - zone_soa.expire(), - zone_soa.minimum(), - )); - - records.update_data(|rr| rr.rtype() == Rtype::SOA, new_soa); - ws.state.previous_serial = Some(serial); - } - } + let zone_soa_rr = records.find_soa().expect("should exist"); + let new_soa = ws.update_soa_serial(zone_soa_rr.first())?; + records.update_data(|rr| rr.rtype() == Rtype::SOA, new_soa.into_data()); // Find the apex. let (apex, zone_class, ttl, soa_serial) = Self::find_apex(&records).unwrap(); @@ -2039,7 +1943,7 @@ impl WorkSpace { Ok(()) } - fn load_apex_records(&self, iss: &mut IncrementalSigningState) -> Result<(), Error> { + fn load_apex_records(&mut self, iss: &mut IncrementalSigningState) -> Result<(), Error> { // Assume that the APEX records have been copied from KeySetState to // SignerState. Now update the APEX in new_data. @@ -2105,6 +2009,14 @@ impl WorkSpace { let key = (iss.origin.clone(), Rtype::ZONEMD); iss.new_data.insert(key, records); } + + // Update the SOA serial. + let key = (iss.origin.clone(), Rtype::SOA); + let zone_soa_rr = &iss.new_data.get(&key).expect("SOA should exist")[0]; + let new_soa = self.update_soa_serial(zone_soa_rr)?; + let new_rrset = vec![new_soa]; + iss.new_data.insert(key, new_rrset); + Ok(()) } @@ -2214,6 +2126,136 @@ impl WorkSpace { iss.rrsigs.insert(key, new_sigs[0].0.clone()); Ok(()) } + + fn update_soa_serial(&mut self, old_soa: &Zrd) -> Result { + // Implement SOA serial policies. There are four policies: + // 1) Keep. Copy the serial from the unsigned zone. Refuse to sign + // if the serial did not change. + // 2) Increment. Copy the serial from the unsigned zone but increment + // the serial if the zone needs to be signed an the serial in + // the unsigned zone did not change. + // 3) Unix timestamp. The current time in Unix seconds. Increment if + // that does not result in a higher serial. + // 4) Broken down time (YYYYMMDDnn). The current day plus a serial + // number. Implies increment to generate different serial numbers + // over a day. + + let ZoneRecordData::Soa(zone_soa) = old_soa.data() else { + unreachable!(); + }; + + // Assume that we will change the state. + self.state_changed = true; + + match self.config.serial_policy { + SerialPolicy::Keep => { + if let Some(previous_serial) = self.state.previous_serial { + if zone_soa.serial() <= previous_serial { + return Err( + "Serial policy is Keep but upstream serial did not increase".into() + ); + } + } + + self.state.previous_serial = Some(zone_soa.serial()); + Ok(old_soa.clone()) + } + SerialPolicy::Increment => { + let mut serial = zone_soa.serial(); + if let Some(previous_serial) = self.state.previous_serial { + if serial <= previous_serial { + serial = previous_serial.add(1); + self.state.previous_serial = Some(serial); + + let new_soa = ZoneRecordData::Soa(Soa::new( + zone_soa.mname().clone(), + zone_soa.rname().clone(), + serial, + zone_soa.refresh(), + zone_soa.retry(), + zone_soa.expire(), + zone_soa.minimum(), + )); + let record = Record::new( + old_soa.owner().clone(), + old_soa.class(), + old_soa.ttl(), + new_soa, + ); + + return Ok(record); + } + } + + self.state.previous_serial = Some(serial); + Ok(old_soa.clone()) + } + SerialPolicy::UnixSeconds => { + let mut serial = Serial::now(); + if let Some(previous_serial) = self.state.previous_serial { + if serial <= previous_serial { + serial = previous_serial.add(1); + } + } + + self.state.previous_serial = Some(serial); + + let new_soa = ZoneRecordData::Soa(Soa::new( + zone_soa.mname().clone(), + zone_soa.rname().clone(), + serial, + zone_soa.refresh(), + zone_soa.retry(), + zone_soa.expire(), + zone_soa.minimum(), + )); + + let record = Record::new( + old_soa.owner().clone(), + old_soa.class(), + old_soa.ttl(), + new_soa, + ); + + Ok(record) + } + SerialPolicy::Date => { + let ts = JiffTimestamp::now(); + let zone = Zoned::new(ts, TimeZone::UTC); + let serial = ((zone.year() as u32 * 100 + zone.month() as u32) * 100 + + zone.day() as u32) + * 100; + let mut serial: Serial = serial.into(); + + if let Some(previous_serial) = self.state.previous_serial { + if serial <= previous_serial { + serial = previous_serial.add(1); + } + } + + self.state.previous_serial = Some(serial); + + let new_soa = ZoneRecordData::Soa(Soa::new( + zone_soa.mname().clone(), + zone_soa.rname().clone(), + serial, + zone_soa.refresh(), + zone_soa.retry(), + zone_soa.expire(), + zone_soa.minimum(), + )); + + let record = Record::new( + old_soa.owner().clone(), + old_soa.class(), + old_soa.ttl(), + new_soa, + ); + + Ok(record) + } + } + } } fn remove_nsec_nsec3(iss: &mut IncrementalSigningState) { From 4413126083ee8b82a6da2c5ee4cf9c1944c3ff0f Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Fri, 27 Feb 2026 17:00:01 +0100 Subject: [PATCH 15/20] Add notify command to incremental signing. --- src/commands/signer.rs | 50 +++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/src/commands/signer.rs b/src/commands/signer.rs index 50949d1a..a464b26b 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -1008,26 +1008,7 @@ impl Signer { writer.flush().map_err(|e| format!("flush failed: {e}"))?; - if !ws.config.notify_command.is_empty() { - let output = Command::new(&ws.config.notify_command[0]) - .args(&ws.config.notify_command[1..]) - .output() - .map_err(|e| { - format!( - "unable to create new Command for {}: {e}", - ws.config.notify_command[0] - ) - })?; - if !output.status.success() { - println!("notify command failed with: {}", output.status); - io::stdout() - .write_all(&output.stdout) - .map_err(|e| format!("unable to write to stdout: {e}"))?; - io::stderr() - .write_all(&output.stderr) - .map_err(|e| format!("unable to write to stderr: {e}"))?; - } - } + ws.run_notify_command()?; Ok(()) } @@ -1514,6 +1495,9 @@ impl WorkSpace { println!("incremental signing took {:?}", start.elapsed()); self.incremental_write_output(&iss)?; + + self.run_notify_command()?; + Ok(()) } @@ -2256,6 +2240,32 @@ impl WorkSpace { } } } + + fn run_notify_command(&self) -> Result<(), Error> { + if self.config.notify_command.is_empty() { + return Ok(()); // Nothing to do. + } + + let output = Command::new(&self.config.notify_command[0]) + .args(&self.config.notify_command[1..]) + .output() + .map_err(|e| { + format!( + "unable to create new Command for {}: {e}", + self.config.notify_command[0] + ) + })?; + if !output.status.success() { + println!("notify command failed with: {}", output.status); + io::stdout() + .write_all(&output.stdout) + .map_err(|e| format!("unable to write to stdout: {e}"))?; + io::stderr() + .write_all(&output.stderr) + .map_err(|e| format!("unable to write to stderr: {e}"))?; + } + Ok(()) + } } fn remove_nsec_nsec3(iss: &mut IncrementalSigningState) { From ebbff24787b3f270117b59a41d5d919684904397 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Mon, 2 Mar 2026 16:35:27 +0100 Subject: [PATCH 16/20] Add verbose option. --- src/commands/signer.rs | 47 +++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/src/commands/signer.rs b/src/commands/signer.rs index a464b26b..a78ad925 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -81,6 +81,9 @@ pub struct Signer { #[arg(short = 'c')] signer_config: PathBuf, + #[arg(short = 'v')] + verbose: bool, + /// Subcommand #[command(subcommand)] cmd: Commands, @@ -449,6 +452,7 @@ impl Signer { config_changed: false, state: signer_state, state_changed: false, + verbose: self.verbose, }; let mut res = Ok(()); @@ -1372,6 +1376,7 @@ struct WorkSpace { config_changed: bool, state: SignerState, state_changed: bool, + verbose: bool, } impl WorkSpace { @@ -1438,10 +1443,12 @@ impl WorkSpace { let now = self.faketime_or_now(); if now > self.state.last_signature_refresh.clone() + self.config.signature_refresh_interval { - println!( - "refresh signatures: {} > {} + {:?}", - now, self.state.last_signature_refresh, self.config.signature_refresh_interval - ); + if self.verbose { + println!( + "refresh signatures: {} > {} + {:?}", + now, self.state.last_signature_refresh, self.config.signature_refresh_interval + ); + } refresh_signatures = true; } @@ -1454,14 +1461,18 @@ impl WorkSpace { let start = Instant::now(); load_signed_zone(&mut iss, &self.config.zonefile_out).unwrap(); - println!("loading signed zone took {:?}", start.elapsed()); + if self.verbose { + println!("loading signed zone took {:?}", start.elapsed()); + } self.handle_nsec_nsec3(&mut iss)?; if load_unsigned { let start = Instant::now(); load_unsigned_zone(&mut iss, &self.config.zonefile_in).unwrap(); - println!("loading new unsigned zone took {:?}", start.elapsed()); + if self.verbose { + println!("loading new unsigned zone took {:?}", start.elapsed()); + } } else { // Re-use the signed data. load_signed_only(&mut iss); @@ -1483,7 +1494,9 @@ impl WorkSpace { if !self.config.zonemd.is_empty() { let start = Instant::now(); self.add_zonemd(&mut iss)?; - println!("ZONEMD took {:?}", start.elapsed()); + if self.verbose { + println!("ZONEMD took {:?}", start.elapsed()); + } } if refresh_signatures { @@ -1492,7 +1505,9 @@ impl WorkSpace { self.key_roll_signatures(&mut iss)?; } } - println!("incremental signing took {:?}", start.elapsed()); + if self.verbose { + println!("incremental signing took {:?}", start.elapsed()); + } self.incremental_write_output(&iss)?; @@ -1823,7 +1838,9 @@ impl WorkSpace { let start = Instant::now(); remove_nsec_nsec3(iss); new_nsec3_chain(iss)?; - println!("updating NSEC3 parameters took {:?}", start.elapsed()); + if self.verbose { + println!("updating NSEC3 parameters took {:?}", start.elapsed()); + } return Ok(()); } @@ -1923,7 +1940,9 @@ impl WorkSpace { .map_err(|e| format!("unable write signed zone: {e}"))?; } } - println!("writing output took {:?}", start.elapsed()); + if self.verbose { + println!("writing output took {:?}", start.elapsed()); + } Ok(()) } @@ -2059,7 +2078,9 @@ impl WorkSpace { //all.sort_by(|e1, e2| CanonicalOrd::canonical_cmp(*e1, *e2)); all.par_sort_by(|e1, e2| CanonicalOrd::canonical_cmp(*e1, *e2)); - println!("ZONEMD prepare and sort took {:?}", start.elapsed()); + if self.verbose { + println!("ZONEMD prepare and sort took {:?}", start.elapsed()); + } let start = Instant::now(); @@ -2095,7 +2116,9 @@ impl WorkSpace { zonemd_records.push(record); } - println!("ZONEMD hash took {:?}", start.elapsed()); + if self.verbose { + println!("ZONEMD hash took {:?}", start.elapsed()); + } let key = (iss.origin.clone(), Rtype::ZONEMD); let mut new_sigs = vec![]; From 25745b252c9a25e2b9eca3d4b9f8003c13e199d9 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Mon, 2 Mar 2026 16:41:15 +0100 Subject: [PATCH 17/20] Update domain. --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 647ceac3..496cf488 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -358,7 +358,7 @@ dependencies = [ [[package]] name = "domain" version = "0.11.1" -source = "git+https://github.com/NLnetLabs/domain.git?branch=main#3172a68fe3c6734d3b083e6475d65c563b3e7f23" +source = "git+https://github.com/NLnetLabs/domain.git?branch=main#622d48f82a6272c96282964aa36c1af3d32d776e" dependencies = [ "arc-swap", "bumpalo", @@ -404,7 +404,7 @@ dependencies = [ [[package]] name = "domain-macros" version = "0.11.1" -source = "git+https://github.com/NLnetLabs/domain.git?branch=main#3172a68fe3c6734d3b083e6475d65c563b3e7f23" +source = "git+https://github.com/NLnetLabs/domain.git?branch=main#622d48f82a6272c96282964aa36c1af3d32d776e" dependencies = [ "proc-macro2", "quote", @@ -1685,7 +1685,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", From 61efafba5b2165b8b2f812220e0cc2912e66d686 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Tue, 3 Mar 2026 16:05:21 +0100 Subject: [PATCH 18/20] Pass-through mode. --- src/commands/signer.rs | 190 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 186 insertions(+), 4 deletions(-) diff --git a/src/commands/signer.rs b/src/commands/signer.rs index a78ad925..d3b87cbf 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -205,6 +205,9 @@ enum SetCommands { SerialPolicy { serial_policy: SerialPolicy, }, + PassThroughMode { + pass_through_mode: PassThroughMode, + }, NotifyCommand { args: Vec, }, @@ -380,6 +383,7 @@ impl Signer { notify_command: Vec::new(), signature_refresh_interval: Duration::from_secs(FIFTEEN_MINUTES), key_roll_time: Duration::from_secs(ONE_DAY), + pass_through_mode: Default::default(), faketime: None, }; let json = serde_json::to_string_pretty(&sc).expect("should not fail"); @@ -528,6 +532,11 @@ impl Signer { // The entire zone is signed, clear key_roll. ws.state.key_roll = None; + if !matches!(ws.config.pass_through_mode, PassThroughMode::Off) { + ws.sign_pass_through()?; + return Ok(()); + } + // Read the zone file. let origin = ws.keyset_state.keyset.name().to_bytes(); let mut records = self.load_zone(&env.in_cwd(&ws.config.zonefile_in), origin.clone())?; @@ -1104,8 +1113,15 @@ impl Signer { // TODO: Create an issue for the original ldns-signzone or // release a fixed version of ldns-signzone that strips // NSEC(3)s. - // - // TODO: Support partial and re-signing. + + // Skip DNSKEY, CDS, CDNSKEY. We should check the apex + // records from keyset to see what should be deleted. + if (matches!(record.rtype(), Rtype::DNSKEY | Rtype::CDS | Rtype::CDNSKEY)) + && *record.owner() == origin + { + continue; + } + if !matches!( record.rtype(), Rtype::RRSIG | Rtype::NSEC | Rtype::NSEC3 | Rtype::NSEC3PARAM @@ -1417,6 +1433,21 @@ impl WorkSpace { SetCommands::SerialPolicy { serial_policy } => { self.config.serial_policy = serial_policy; } + SetCommands::PassThroughMode { pass_through_mode } => { + // Make sure serial_policy is Keep to avoid suprises. + if !matches!(self.config.serial_policy, SerialPolicy::Keep) { + return Err( + "'serial-policy' has to be 'keep' when enabling pass-through mode".into(), + ); + } + + // Make sure ZONEMD is off. + if !self.config.zonemd.is_empty() { + return Err("'zone-md' has to be empty when enabling pass-through mode".into()); + } + + self.config.pass_through_mode = pass_through_mode; + } SetCommands::NotifyCommand { args } => { self.config.notify_command = args; } @@ -1439,6 +1470,11 @@ impl WorkSpace { let apex_changed = self.handle_keyset_changed(); + if !matches!(self.config.pass_through_mode, PassThroughMode::Off) { + self.sign_pass_through()?; + return Ok(()); + } + let mut refresh_signatures = false; let now = self.faketime_or_now(); if now > self.state.last_signature_refresh.clone() + self.config.signature_refresh_interval @@ -1709,7 +1745,17 @@ impl WorkSpace { } let key = ((*owner).clone(), **rtype); - if **rtype == Rtype::NSEC3 { + if **rtype == Rtype::NSEC { + let record = iss.nsecs.get(&key.0).expect("NSEC record should exist"); + let records = [record.clone()]; + sign_records( + &records, + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + } else if **rtype == Rtype::NSEC3 { let record = iss.nsec3s.get(&key.0).expect("NSEC3 record should exist"); let records = [record.clone()]; sign_records( @@ -1770,7 +1816,7 @@ impl WorkSpace { if apex_extra != self.state.apex_extra { println!( - "APEX types changed: from {:?} to {apex_extra:?}", + "APEX extra changed: from {:?} to {apex_extra:?}", self.state.apex_extra ); apex_changed = true; @@ -2023,6 +2069,91 @@ impl WorkSpace { Ok(()) } + fn load_pass_through_dnskey(&mut self, iss: &mut IncrementalSigningState) -> Result<(), Error> { + // Assume that the APEX records have been copied from KeySetState to + // SignerState. Now update the APEX in new_data. + + let mut dnskey_records = vec![]; + let mut rrsig_records = vec![]; + + for r in &self.state.apex_extra { + let zonefile = + domain::zonefile::inplace::Zonefile::from((r.to_string() + "\n").as_ref() as &str); + for entry in zonefile { + let entry = entry.map_err::(|e| format!("bad entry: {e}\n").into())?; + + // We only care about records in a zonefile + let Entry::Record(record) = entry else { + continue; + }; + + if record.rtype() != Rtype::DNSKEY && record.rtype() != Rtype::RRSIG { + continue; + } + + let owner = record.owner().to_name::(); + let data = record.data().clone().try_flatten_into().unwrap(); + let r = Record::new(owner.clone(), record.class(), record.ttl(), data); + + if r.rtype() == Rtype::RRSIG { + let ZoneRecordData::Rrsig(rrsig) = r.data() else { + panic!("RRSIG expected"); + }; + if rrsig.type_covered() != Rtype::DNSKEY { + continue; + } + rrsig_records.push(r); + } else { + dnskey_records.push(r); + } + } + } + + match self.config.pass_through_mode { + PassThroughMode::Off => unreachable!(), + PassThroughMode::CopyDnskeyRrset => { + let key = ( + dnskey_records + .first() + .ok_or("at least one DNSKEY expected")? + .owner() + .clone(), + Rtype::DNSKEY, + ); + iss.new_data.insert(key.clone(), dnskey_records); + iss.rrsigs.insert(key, rrsig_records); + } + PassThroughMode::MergeDnskeySignatures => { + // Make sure the old and new DNSKEY RRsets are the same. + let key = (iss.origin.clone(), Rtype::DNSKEY); + let Some(old_dnskey_records) = iss.old_data.get(&key) else { + return Err("A DNSKEY RRset should exist in the input zone".into()); + }; + let mut old_dnskey_records = old_dnskey_records.clone(); + old_dnskey_records.sort(); + dnskey_records.sort(); + if *old_dnskey_records != dnskey_records { + return Err( + "DNSKEY RRset in input has to be same as the DNSKEY RRset in keyset".into(), + ); + } + let Some(rrsigs) = iss.rrsigs.get(&key) else { + return Err("RRSIGs expected for DNSKEY RRset".into()); + }; + let mut rrsigs = rrsigs.clone(); + rrsigs.append(&mut rrsig_records); + iss.rrsigs.insert(key, rrsigs); + } + } + + let key = (iss.origin.clone(), Rtype::ZONEMD); + if iss.new_data.contains_key(&key) { + return Err("Pass-through is not possible for zone input with ZONEMD".into()); + } + + Ok(()) + } + fn add_zonemd(&self, iss: &mut IncrementalSigningState) -> Result<(), Error> { // Get the SOA record. We need that for the Serial and for the // TTL. @@ -2289,6 +2420,38 @@ impl WorkSpace { } Ok(()) } + + fn sign_pass_through(&mut self) -> Result<(), Error> { + // Clear key_tags and key_roll to trigger resigning when + // pass-through mode is turned off. Also clear keyset_state_modified + // to trigger a reload of the keyset state when pass-through is + // turned off. + if !self.state.key_tags.is_empty() { + self.state.key_tags = HashSet::new(); + self.state.keyset_state_modified = Timestamp::from(0).into(); + self.state_changed = true; + } + if self.state.key_roll.is_some() { + self.state.key_roll = None; + self.state_changed = true; + } + + let mut iss = IncrementalSigningState::new(self)?; + + let start = Instant::now(); + load_signed_zone(&mut iss, &self.config.zonefile_in).unwrap(); + if self.verbose { + println!("loading signed zone took {:?}", start.elapsed()); + } + + // Re-use the signed data. + load_signed_only(&mut iss); + + self.load_pass_through_dnskey(&mut iss)?; + + self.incremental_write_output(&iss)?; + Ok(()) + } } fn remove_nsec_nsec3(iss: &mut IncrementalSigningState) { @@ -2437,6 +2600,9 @@ struct SignerConfig { /// Maxmimum time to resign all records with new ZSKs or CSKs. key_roll_time: Duration, + #[serde(default)] + pass_through_mode: PassThroughMode, + /// Fake time to use when signing. /// /// This is need for integration tests. @@ -2471,6 +2637,22 @@ struct SignerState { last_signature_refresh: UnixTime, } +#[derive(Clone, Debug, Default, Deserialize, Serialize, clap::ValueEnum)] +enum PassThroughMode { + /// Pass-through is disabled. + #[default] + Off, + + /// Copy the DNSKEY RRset plus signatures from keyset into an already + /// signed zone. The operator has to make sure that the DNSKEY RRset + /// contains the public key of the key that signed the zone. + CopyDnskeyRrset, + + /// Add the DNSKEY signatures from keyset. This requires that the DNSKEY + /// RRset in the input zone is equal to the one from keyset. + MergeDnskeySignatures, +} + type RtypeSet = HashSet; type ChangesValue = (RtypeSet, RtypeSet); // add set followed by delete set. From 7a2a809142b4ccc5ae923d439349b4739501cea3 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 4 Mar 2026 14:10:56 +0100 Subject: [PATCH 19/20] Support for NSEC was missing. --- src/commands/signer.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/commands/signer.rs b/src/commands/signer.rs index d3b87cbf..8b6f27c4 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -1678,7 +1678,17 @@ impl WorkSpace { } let key = ((*owner).clone(), *rtype); - if *rtype == Rtype::NSEC3 { + if *rtype == Rtype::NSEC { + let record = iss.nsecs.get(&key.0).expect("NSEC record should exist"); + let records = [record.clone()]; + sign_records( + &records, + &iss.keys, + iss.inception, + iss.expiration, + &mut new_sigs, + )?; + } else if *rtype == Rtype::NSEC3 { let record = iss.nsec3s.get(&key.0).expect("NSEC3 record should exist"); let records = [record.clone()]; sign_records( From 3e9185645966aea835edc0f0fbfef23c4cb94bdd Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Fri, 6 Mar 2026 11:16:23 +0100 Subject: [PATCH 20/20] Be more careful and only exclude DNSKEY/CDS/CDNSKEY at apex. --- src/commands/signer.rs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/commands/signer.rs b/src/commands/signer.rs index 8b6f27c4..a8d32ddb 100644 --- a/src/commands/signer.rs +++ b/src/commands/signer.rs @@ -1606,6 +1606,7 @@ impl WorkSpace { let record = iss.nsecs.get(&key.0).expect("NSEC record should exist"); let records = [record.clone()]; sign_records( + &iss.origin, &records, &iss.keys, iss.inception, @@ -1616,6 +1617,7 @@ impl WorkSpace { let record = iss.nsec3s.get(&key.0).expect("NSEC3 record should exist"); let records = [record.clone()]; sign_records( + &iss.origin, &records, &iss.keys, iss.inception, @@ -1625,6 +1627,7 @@ impl WorkSpace { } else { let records = iss.new_data.get(&key).expect("records should exist"); sign_records( + &iss.origin, records, &iss.keys, iss.inception, @@ -1682,6 +1685,7 @@ impl WorkSpace { let record = iss.nsecs.get(&key.0).expect("NSEC record should exist"); let records = [record.clone()]; sign_records( + &iss.origin, &records, &iss.keys, iss.inception, @@ -1692,6 +1696,7 @@ impl WorkSpace { let record = iss.nsec3s.get(&key.0).expect("NSEC3 record should exist"); let records = [record.clone()]; sign_records( + &iss.origin, &records, &iss.keys, iss.inception, @@ -1701,6 +1706,7 @@ impl WorkSpace { } else { let records = iss.new_data.get(&key).expect("records should exist"); sign_records( + &iss.origin, records, &iss.keys, iss.inception, @@ -1759,6 +1765,7 @@ impl WorkSpace { let record = iss.nsecs.get(&key.0).expect("NSEC record should exist"); let records = [record.clone()]; sign_records( + &iss.origin, &records, &iss.keys, iss.inception, @@ -1769,6 +1776,7 @@ impl WorkSpace { let record = iss.nsec3s.get(&key.0).expect("NSEC3 record should exist"); let records = [record.clone()]; sign_records( + &iss.origin, &records, &iss.keys, iss.inception, @@ -1778,6 +1786,7 @@ impl WorkSpace { } else { let records = iss.new_data.get(&key).expect("records should exist"); sign_records( + &iss.origin, records, &iss.keys, iss.inception, @@ -1928,6 +1937,7 @@ impl WorkSpace { let nsec3 = nsec3.clone(); sign_records( + &iss.origin, &[nsec3], &iss.keys, iss.inception, @@ -1943,6 +1953,7 @@ impl WorkSpace { let nsec = nsec.clone(); sign_records( + &iss.origin, &[nsec], &iss.keys, iss.inception, @@ -2264,6 +2275,7 @@ impl WorkSpace { let key = (iss.origin.clone(), Rtype::ZONEMD); let mut new_sigs = vec![]; sign_records( + &iss.origin, &zonemd_records, &iss.keys, iss.inception, @@ -2497,6 +2509,7 @@ fn new_nsec_chain(iss: &mut IncrementalSigningState) -> Result<(), Error> { ); iss.nsecs.insert(record.owner().clone(), record.clone()); sign_records( + &iss.origin, &[record], &iss.keys, iss.inception, @@ -2534,6 +2547,7 @@ fn new_nsec3_chain(iss: &mut IncrementalSigningState) -> Result<(), Error> { // Insert in both old and new data. sign_records( + &iss.origin, &[record], &iss.keys, iss.inception, @@ -2552,6 +2566,7 @@ fn new_nsec3_chain(iss: &mut IncrementalSigningState) -> Result<(), Error> { ); iss.nsec3s.insert(record.owner().clone(), record.clone()); sign_records( + &iss.origin, &[record], &iss.keys, iss.inception, @@ -2981,9 +2996,11 @@ fn initial_diffs(iss: &mut IncrementalSigningState) -> Result<(), Error> { let key = (new_rrset[0].owner().clone(), new_rrset[0].rtype()); if let Some(mut old_rrset) = iss.old_data.remove(&key) { let rtype = new_rrset[0].rtype(); - if rtype == Rtype::DNSKEY || rtype == Rtype::CDS || rtype == Rtype::CDNSKEY { - // These types are signed by the key manager. No need to - // check for changes. + if (rtype == Rtype::DNSKEY || rtype == Rtype::CDS || rtype == Rtype::CDNSKEY) + && *new_rrset[0].owner() == iss.origin + { + // At apex, these types are signed by the key manager. No + // need to check for changes. continue; } old_rrset.sort_by(|a, b| a.as_ref().data().canonical_cmp(b.as_ref().data())); @@ -2991,6 +3008,7 @@ fn initial_diffs(iss: &mut IncrementalSigningState) -> Result<(), Error> { if *old_rrset != *new_rrset && iss.rrsigs.remove(&key).is_some() { sign_records( + &iss.origin, new_rrset, &iss.keys, iss.inception, @@ -4159,6 +4177,7 @@ fn sign_rtype_set( panic!("Expected something for {name}/{rtype}"); }; sign_records( + &iss.origin, records, &iss.keys, iss.inception, @@ -4174,6 +4193,7 @@ fn sign_rtype_set( } fn sign_records( + origin: &Name, records: &[Zrd], keys: &[SigningKey], inception: Timestamp, @@ -4181,7 +4201,9 @@ fn sign_records( new_sigs: &mut Vec<(Vec, Rtype)>, ) -> Result<(), Error> { let rtype = records[0].rtype(); - if rtype == Rtype::DNSKEY || rtype == Rtype::CDS || rtype == Rtype::CDNSKEY { + if (rtype == Rtype::DNSKEY || rtype == Rtype::CDS || rtype == Rtype::CDNSKEY) + && records[0].owner() == origin + { // These records get signed with the KSK(s). Don't touch // the signatures. return Ok(());