diff --git a/doc/manual/build/man/dnst-keyset.1 b/doc/manual/build/man/dnst-keyset.1 index 8a2f0e8..45c3b28 100644 --- a/doc/manual/build/man/dnst-keyset.1 +++ b/doc/manual/build/man/dnst-keyset.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "DNST-KEYSET" "1" "Feb 25, 2026" "0.1.1-dev" "dnst" +.TH "DNST-KEYSET" "1" "Mar 02, 2026" "0.1.1-dev" "dnst" .SH NAME dnst-keyset \- Manage DNSSEC signing keys for a domain .SH SYNOPSIS @@ -35,7 +35,7 @@ dnst-keyset \- Manage DNSSEC signing keys for a domain \fBdnst keyset\fP \fB\-c \fP \fB[OPTIONS]\fP \fB\fP \fB[ARGS]\fP .SH DESCRIPTION .sp -The \fBkeyset\fP subcommand manages a set of DNSSEC (\X'tty: link https://www.rfc-editor.org/rfc/rfc9364'\fI\%RFC 9364\fP\X'tty: link') signing keys. +The \fBkeyset\fP subcommand manages a set of DNSSEC (\fI\%RFC 9364\fP) signing keys. This subcommand is meant to be part of a DNSSEC signing solution. The \fBkeyset\fP subcommand manages signing keys and generates a signed DNSKEY RRset. A separate zone signer (not part of dnst) is expected to use the zone @@ -240,6 +240,15 @@ steps must be done manually in order to be able to insert extra manual steps. .sp The \fBreport\fP and \fBdone\fP automations require that keyset has network access to all nameservers of the zone and all nameservers of the parent. +.sp +The configuration variables \fBautoremove\fP and \fBautoremove\-delay\fP +control the automatic removal of keys that are no longer needed. +The variable \fBautoremove\fP defaults to false. +In this case, stale keys have to be removed manually. +When \fBautoremove\fP is set to true, the \fBcron\fP subcommand checks if any +keys have been stale for at least \fBautoremove\-delay\fP, and if so, removes +those keys. +The \fBautoremove\-delay\fP variable defaults to one week. .SS HSM Support (KMIP) .sp The keyset subcommand supports keys in Hardware Security Modules (HSM) through @@ -786,11 +795,19 @@ Set a command to to run when the DS records in the parent zone need to be updated. This command can, for example, alert the operator or use an API provided by the parent zone to update the DS records automatically. +.IP \(bu 2 +fake\-time +.sp +Set the \(aqwall clock\(aq time to be used for testing. +The argument is either the Unix time as seconds since Epoch or the string +\(aqoff\(aq to disable fake\-time. .UNINDENT .IP \(bu 2 show .sp Show all configuration variables. +.sp +Note that \(aqfake\-time\(aq is only printed when it is set. .IP \(bu 2 cron .sp diff --git a/doc/manual/source/man/dnst-keyset.rst b/doc/manual/source/man/dnst-keyset.rst index 330d22a..bcccf3c 100644 --- a/doc/manual/source/man/dnst-keyset.rst +++ b/doc/manual/source/man/dnst-keyset.rst @@ -776,10 +776,18 @@ The keyset subcommand provides the following commands: This command can, for example, alert the operator or use an API provided by the parent zone to update the DS records automatically. + * fake-time + + Set the 'wall clock' time to be used for testing. + The argument is either the Unix time as seconds since Epoch or the string + 'off' to disable fake-time. + * show Show all configuration variables. + Note that 'fake-time' is only printed when it is set. + * cron Execute any automatic steps such a refreshing signatures or automatic steps diff --git a/src/commands/keyset/cmd.rs b/src/commands/keyset/cmd.rs index 400c4c4..e3d237e 100644 --- a/src/commands/keyset/cmd.rs +++ b/src/commands/keyset/cmd.rs @@ -59,8 +59,9 @@ use std::io::{self, Write}; use std::net::{IpAddr, SocketAddr}; use std::path::{absolute, Path, PathBuf}; use std::process::Command; +use std::str::FromStr; use std::sync::Mutex; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::{Duration, UNIX_EPOCH}; use tokio::net::TcpStream; #[cfg(feature = "kmip")] use tracing::{debug, error, warn}; @@ -163,6 +164,10 @@ pub struct Keyset { /// treats Option special. type OptDuration = Option; +/// Type for an optional UnixTime. A separate type is needed because CLAP +/// treats Option special. +type OptUnixTime = Option; + /// The subcommands of the keyset utility. #[allow(clippy::large_enum_variant)] #[derive(Clone, Debug, Subcommand)] @@ -474,6 +479,14 @@ enum SetCommands { /// Command and arguments. 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 = parse_opt_unixtime)] + opt_unixtime: OptUnixTime, + }, } /// The various subcommands of a key roll command. @@ -703,6 +716,7 @@ impl Keyset { autoremove: false, autoremove_delay: DEFAULT_AUTOREMOVE_DELAY, update_ds_command: Vec::new(), + faketime: None, }; // Create the parent directies. @@ -760,6 +774,7 @@ impl Keyset { pools: HashMap::new(), }; + let now = ws.faketime_or_now(); match self.cmd { Commands::Create { .. } => unreachable!(), Commands::Init => { @@ -862,7 +877,7 @@ impl Keyset { if let Err(keyset::Error::Wait(remain)) = res { println!( "Wait until {} to let caches expire", - UnixTime::now() + remain + now.clone() + remain ); } else if let Err(e) = res { return Err(format!( @@ -1009,10 +1024,14 @@ impl Keyset { println!(); } - if sig_renew(&ws.state.dnskey_rrset, &ws.config.dnskey_remain_time) { + if sig_renew( + &ws.state.dnskey_rrset, + &ws.config.dnskey_remain_time, + now.clone(), + ) { println!("DNSKEY RRSIG(s) need to be renewed"); } - if sig_renew(&ws.state.cds_rrset, &ws.config.cds_remain_time) { + if sig_renew(&ws.state.cds_rrset, &ws.config.cds_remain_time, now.clone()) { println!("CDS/CDNSKEY RRSIG(s) need to be renewed"); } @@ -1200,14 +1219,25 @@ impl Keyset { println!("autoremove: {:?}", ws.config.autoremove); println!("autoremove-delay: {:?}", ws.config.autoremove_delay); println!("update_ds_command: {:?}", ws.config.update_ds_command); + // Only print faketime when it exists. + if let Some(faketime) = &ws.config.faketime { + println!( + "fake-time: {}", + >::into(faketime.clone()).as_secs() + ); + } } Commands::Cron => { - if sig_renew(&ws.state.dnskey_rrset, &ws.config.dnskey_remain_time) { + if sig_renew( + &ws.state.dnskey_rrset, + &ws.config.dnskey_remain_time, + now.clone(), + ) { println!("DNSKEY RRSIG(s) need to be renewed"); ws.update_dnskey_rrset(env, false)?; ws.state_changed = true; } - if sig_renew(&ws.state.cds_rrset, &ws.config.cds_remain_time) { + if sig_renew(&ws.state.cds_rrset, &ws.config.cds_remain_time, now.clone()) { println!("CDS/CDNSKEY RRSIGs need to be renewed"); ws.create_cds_rrset(env, false)?; ws.state_changed = true; @@ -1342,7 +1372,7 @@ impl Keyset { let autoremove = ws.config.autoremove; let autoremove_delay = ws.config.autoremove_delay; - let now = UnixTime::now(); + let now = ws.faketime_or_now(); if autoremove { let key_urls: Vec<_> = ws .state @@ -1404,11 +1434,13 @@ impl Keyset { cron_next.push(compute_cron_next( &ws.state.dnskey_rrset, &ws.config.dnskey_remain_time, + now.clone(), )); cron_next.push(compute_cron_next( &ws.state.cds_rrset, &ws.config.cds_remain_time, + now, )); let need_algorithm_roll = ws.algorithm_roll_needed(); @@ -1500,25 +1532,25 @@ impl Keyset { &mut cron_next, ); - cron_next_auto_report_expire_done( + ws.cron_next_auto_report_expire_done( &ws.config.auto_ksk, &[RollType::KskRoll, RollType::KskDoubleDsRoll], &ws.state, &mut cron_next, )?; - cron_next_auto_report_expire_done( + ws.cron_next_auto_report_expire_done( &ws.config.auto_zsk, &[RollType::ZskRoll, RollType::ZskDoubleSignatureRoll], &ws.state, &mut cron_next, )?; - cron_next_auto_report_expire_done( + ws.cron_next_auto_report_expire_done( &ws.config.auto_csk, &[RollType::CskRoll], &ws.state, &mut cron_next, )?; - cron_next_auto_report_expire_done( + ws.cron_next_auto_report_expire_done( &ws.config.auto_algorithm, &[RollType::AlgorithmRoll], &ws.state, @@ -1660,6 +1692,11 @@ struct KeySetConfig { /// Command to run when the DS records at the parent need updating. update_ds_command: Vec, + + /// Fake time to use when signing. + /// + /// This is needed for integration tests. + faketime: Option, } /// Configuration for key roll automation. @@ -2004,6 +2041,7 @@ impl WorkSpace { SetCommands::UpdateDsCommand { args } => { self.config.update_ds_command = args; } + SetCommands::FakeTime { opt_unixtime } => self.config.faketime = opt_unixtime, } self.config_changed = true; Ok(()) @@ -2070,6 +2108,7 @@ impl WorkSpace { /// Implementation of the Import subcommands. fn import_command(&mut self, subcommand: ImportCommands, env: &impl Env) -> Result<(), Error> { + let now = self.faketime_or_now(); match subcommand { ImportCommands::PublicKey { path } => { let public_data = std::fs::read_to_string(&path) @@ -2088,7 +2127,7 @@ impl WorkSpace { public_key_url.clone(), public_key.data().algorithm(), public_key.data().key_tag(), - UnixTime::now(), + now.clone(), true, ) .map_err(|e| format!("unable to add public key {public_key_url}: {e}\n"))?; @@ -2102,7 +2141,7 @@ impl WorkSpace { // unconditionally. self.state .keyset - .set_visible(&public_key_url, UnixTime::now()) + .set_visible(&public_key_url, now) .expect("should not happen"); } ImportCommands::Ksk { subcommand } => { @@ -2131,6 +2170,7 @@ impl WorkSpace { subcommand: ImportKeyCommands, key_variant: KeyVariant, ) -> Result<(), Error> { + let now = self.faketime_or_now(); let (public_key_url, private_key_url, algorithm, key_tag, coupled) = match subcommand { ImportKeyCommands::File { path, @@ -2239,7 +2279,7 @@ impl WorkSpace { Some(private_key_url.clone()), algorithm, key_tag, - UnixTime::now(), + now.clone(), Available::Available, ) .map_err(|e| { @@ -2255,7 +2295,7 @@ impl WorkSpace { Some(private_key_url.clone()), algorithm, key_tag, - UnixTime::now(), + now.clone(), Available::Available, ) .map_err(|e| format!("unable to add ZSK {public_key_url}: {e}\n"))?; @@ -2269,7 +2309,7 @@ impl WorkSpace { Some(private_key_url.clone()), algorithm, key_tag, - UnixTime::now(), + now.clone(), Available::Available, ) .map_err(|e| format!("unable to add CSK {public_key_url}: {e}\n"))?; @@ -2288,7 +2328,7 @@ impl WorkSpace { // now. Just set it unconditionally. self.state .keyset - .set_visible(&public_key_url, UnixTime::now()) + .set_visible(&public_key_url, now.clone()) .expect("should not happen"); self.state @@ -2312,7 +2352,7 @@ impl WorkSpace { // now. Just set it unconditionally. self.state .keyset - .set_ds_visible(&public_key_url, UnixTime::now()) + .set_ds_visible(&public_key_url, now.clone()) .expect("should not happen"); } if set_rrsig_visible { @@ -2321,7 +2361,7 @@ impl WorkSpace { // now. Just set it unconditionally. self.state .keyset - .set_rrsig_visible(&public_key_url, UnixTime::now()) + .set_rrsig_visible(&public_key_url, now) .expect("should not happen"); } Ok(()) @@ -2752,6 +2792,7 @@ impl WorkSpace { /// Create a new CSK key or KSK and ZSK keys if use_csk is false. fn new_csk_or_ksk_zsk(&mut self, env: &impl Env) -> Result<(Vec, Vec), Error> { + let now = self.faketime_or_now(); let (new_stored, new_urls) = if self.config.use_csk { let mut new_urls = Vec::new(); @@ -2766,7 +2807,7 @@ impl WorkSpace { Some(csk_priv_url.to_string()), algorithm, key_tag, - UnixTime::now(), + now, Available::Available, ) .map_err(|e| format!("unable to add CSK {csk_pub_url}: {e}\n"))?; @@ -2787,7 +2828,7 @@ impl WorkSpace { Some(ksk_priv_url.to_string()), algorithm, key_tag, - UnixTime::now(), + now.clone(), Available::Available, ) .map_err(|e| format!("unable to add KSK {ksk_pub_url}: {e}\n"))?; @@ -2803,7 +2844,7 @@ impl WorkSpace { Some(zsk_priv_url.to_string()), algorithm, key_tag, - UnixTime::now(), + now, Available::Available, ) .map_err(|e| format!("unable to add ZSK {zsk_pub_url}: {e}\n"))?; @@ -2847,6 +2888,7 @@ impl WorkSpace { /// Collect all keys where present() returns true and sign the DNSKEY RRset /// with all KSK and CSK (KSK state) where signer() returns true. fn update_dnskey_rrset(&mut self, env: &impl Env, verbose: bool) -> Result<(), Error> { + let now = self.faketime_or_now(); let mut dnskeys = Vec::new(); // Clone needed because of public_key_from_url takes &mut KeySetState. let keys = self.state.keyset.keys().clone(); @@ -2864,9 +2906,9 @@ impl WorkSpace { dnskeys.push(public_key); } } - let now = Timestamp::now().into_int(); - let inception = (now - self.config.dnskey_inception_offset.as_secs() as u32).into(); - let expiration = (now + self.config.dnskey_signature_lifetime.as_secs() as u32).into(); + let now_u32 = Into::::into(now).as_secs() as u32; + let inception = (now_u32 - self.config.dnskey_inception_offset.as_secs() as u32).into(); + let expiration = (now_u32 + self.config.dnskey_signature_lifetime.as_secs() as u32).into(); let mut sigs = Vec::new(); for (k, v) in &keys { @@ -2985,6 +3027,7 @@ impl WorkSpace { /// The CDS and CDNSKEY RRsets contain the keys where at_parent() returns /// true. The RRsets are signed with all keys that sign the DNSKEY RRset. fn create_cds_rrset(&mut self, env: &impl Env, verbose: bool) -> Result<(), Error> { + let now = self.faketime_or_now(); let digest_alg = self.config.ds_algorithm.to_digest_algorithm(); let mut cds_list = Vec::new(); let mut cdnskey_list = Vec::new(); @@ -3007,9 +3050,9 @@ impl WorkSpace { // Need to sign } - let now = Timestamp::now().into_int(); - let inception = (now - self.config.cds_inception_offset.as_secs() as u32).into(); - let expiration = (now + self.config.cds_signature_lifetime.as_secs() as u32).into(); + let now_u32 = Into::::into(now).as_secs() as u32; + let inception = (now_u32 - self.config.cds_inception_offset.as_secs() as u32).into(); + let expiration = (now_u32 + self.config.cds_signature_lifetime.as_secs() as u32).into(); let mut cds_sigs = Vec::new(); let mut cdnskey_sigs = Vec::new(); @@ -3276,6 +3319,8 @@ impl WorkSpace { /// Start a KSK roll. fn start_ksk_roll(&mut self, env: &impl Env, verbose: bool) -> Result, Error> { + let now = self.faketime_or_now(); + let roll_type = RollType::KskRoll; assert!(!self.state.keyset.keys().is_empty()); @@ -3322,7 +3367,7 @@ impl WorkSpace { Some(ksk_priv_url.to_string()), algorithm, key_tag, - UnixTime::now(), + now, Available::Available, ) .map_err(|e| format!("unable to add KSK {ksk_pub_url}: {e}\n"))?; @@ -3351,6 +3396,8 @@ impl WorkSpace { /// Start a ZSK roll. fn start_zsk_roll(&mut self, env: &impl Env, verbose: bool) -> Result, Error> { + let now = self.faketime_or_now(); + let roll_type = RollType::ZskRoll; assert!(!self.state.keyset.keys().is_empty()); @@ -3399,7 +3446,7 @@ impl WorkSpace { Some(zsk_priv_url.to_string()), algorithm, key_tag, - UnixTime::now(), + now, Available::Available, ) .map_err(|e| format!("unable to add ZSK {zsk_pub_url}: {e}\n"))?; @@ -3553,6 +3600,7 @@ impl WorkSpace { match_keytype: impl Fn(KeyType) -> Option, start_roll: impl Fn(&mut WorkSpace, Env, bool) -> Result, Error>, ) -> Result<(), Error> { + let now = self.faketime_or_now(); if let Some(validity) = validity { if auto.start { // If there is no conficting roll, and this @@ -3585,7 +3633,7 @@ impl WorkSpace { }) .min(); if let Some(next) = next { - if next < UnixTime::now() { + if next < now { start_roll(self, env, false)?; self.state_changed = true; } @@ -3609,6 +3657,7 @@ impl WorkSpace { roll_list: &[RollType], env: &impl Env, ) -> Result<(), Error> { + let now = self.faketime_or_now(); if auto.report { // If there is currently a roll in one of the // propagation states and this flags is set and all @@ -3628,6 +3677,7 @@ impl WorkSpace { &self.state, report_state, &mut self.state_changed, + now.clone(), ) .await { @@ -3695,6 +3745,7 @@ impl WorkSpace { &self.state, report_state, &mut self.state_changed, + now.clone(), ) .await { @@ -3743,6 +3794,166 @@ impl WorkSpace { curr_algs != new_algs } + /// Helper function that either returns the configured fake time or the + /// current time. + fn faketime_or_now(&self) -> UnixTime { + self.config.faketime.clone().unwrap_or(UnixTime::now()) + } + + /// Check whether automatic actions are done or not. If not, return until + /// when to wait to try again. + fn check_auto_actions( + &self, + actions: &[Action], + report_state: &Mutex, + ) -> AutoActionsResult { + let now = self.faketime_or_now(); + for a in actions { + match a { + Action::UpdateDnskeyRrset + | Action::CreateCdsRrset + | Action::RemoveCdsRrset + | Action::UpdateDsRrset + | Action::UpdateRrsig => (), + Action::ReportDnskeyPropagated | Action::WaitDnskeyPropagated => { + let report_state_locked = report_state.lock().expect("lock() should not fail"); + if let Some(dnskey_status) = &report_state_locked.dnskey { + match dnskey_status { + AutoReportActionsResult::Wait(next) => { + return AutoActionsResult::Wait(next.clone()) + } + AutoReportActionsResult::Report(_) => continue, + } + } + drop(report_state_locked); + + // No status, request cron + return AutoActionsResult::Wait(now); + } + Action::ReportDsPropagated | Action::WaitDsPropagated => { + let report_state_locked = report_state.lock().expect("lock() should not fail"); + if let Some(ds_status) = &report_state_locked.ds { + match ds_status { + AutoReportActionsResult::Wait(next) => { + return AutoActionsResult::Wait(next.clone()) + } + AutoReportActionsResult::Report(_) => continue, + } + } + drop(report_state_locked); + + // No status, request cron + return AutoActionsResult::Wait(now); + } + Action::ReportRrsigPropagated | Action::WaitRrsigPropagated => { + let report_state_locked = report_state.lock().expect("lock() should not fail"); + if let Some(rrsig_status) = &report_state_locked.rrsig { + match rrsig_status { + AutoReportRrsigResult::Wait(next) + | AutoReportRrsigResult::WaitRecord { next, .. } + | AutoReportRrsigResult::WaitNextSerial { next, .. } + | AutoReportRrsigResult::WaitSoa { next, .. } => { + return AutoActionsResult::Wait(next.clone()) + } + AutoReportRrsigResult::Report(_) => continue, + } + } + drop(report_state_locked); + + // No status, request cron + return AutoActionsResult::Wait(now); + } + } + } + AutoActionsResult::Ok + } + + /// This function computes when next to try to move to the next state. + /// + /// For the Report and Wait actions that involves checking when propagation + /// should be tested again. For the expire step it computes when the + /// keyset object in the domain library accepts the cache_expired1 or + /// cache_expired2 methods. + fn cron_next_auto_report_expire_done( + &self, + auto: &AutoConfig, + roll_list: &[RollType], + kss: &KeySetState, + cron_next: &mut Vec>, + ) -> Result<(), Error> { + let now = self.faketime_or_now(); + if auto.report { + // If there is currently a roll in one of the propagation + // states and this flags is set take when to check again for + // actions to complete + for r in roll_list { + if let Some(state) = kss.keyset.rollstates().get(r) { + let report_state = kss.internal.get(r).expect("should not fail"); + let report_state = match state { + RollState::Propagation1 => &report_state.propagation1, + RollState::Propagation2 => &report_state.propagation2, + _ => continue, + }; + let actions = kss.keyset.actions(*r); + match self.check_auto_actions(&actions, report_state) { + AutoActionsResult::Ok => { + // All actions are ready. Request cron. + cron_next.push(Some(now.clone())); + } + AutoActionsResult::Wait(next) => cron_next.push(Some(next)), + } + } + } + } + + if auto.expire { + // If there is currently a roll in one of the cache expire + // states and this flag is set, use the remaining time until caches + // are expired. Try to issue the cache_expire[12] method on a + // clone of keyset. + let mut keyset = kss.keyset.clone(); + for r in roll_list { + if let Some(state) = keyset.rollstates().get(r) { + let actions = match state { + RollState::CacheExpire1(_) => keyset.cache_expired1(*r), + RollState::CacheExpire2(_) => keyset.cache_expired2(*r), + _ => continue, + }; + if let Err(keyset::Error::Wait(remain)) = actions { + cron_next.push(Some(now.clone() + remain)); + continue; + } + let _ = actions + .map_err(|e| format!("cache_expired[12] failed for state {r:?}: {e}"))?; + + // Time to call cron. Report the current time. + cron_next.push(Some(now.clone())); + } + } + } + + if auto.done { + // If there is current a roll in the done state and all + // and this flag is set, take when the check again for actions to + // complete + for r in roll_list { + if let Some(RollState::Done) = kss.keyset.rollstates().get(r) { + let report_state = kss.internal.get(r).expect("should not fail"); + match self.check_auto_actions(&kss.keyset.actions(*r), &report_state.done) { + AutoActionsResult::Ok => { + // All actions are ready. Request cron. + cron_next.push(Some(now.clone())); + } + AutoActionsResult::Wait(next) => { + cron_next.push(Some(next)); + } + } + } + } + } + Ok(()) + } + /// Write config to a file. fn write_config(&self, keyset_conf: &PathBuf) -> Result<(), Error> { let json = serde_json::to_string_pretty(&self.config).expect("should not fail"); @@ -3900,18 +4111,38 @@ fn parse_opt_duration(value: &str) -> Result, Error> { Ok(Some(duration)) } +/// Parse a UnixTime from string. +/// +/// Those accepts both both a seconds value and a broken down time value +/// without punctuation. +fn parse_unixtime(value: &str) -> Result { + let timestamp = Timestamp::from_str(value) + .map_err(|e| format!("unable to parse Unix time {value}: {e}"))?; + Ok(UnixTime::from(timestamp)) +} + +/// Parse an optional UnixTime from a string but also allow 'off' to signal +/// no UnixTime. +fn parse_opt_unixtime(value: &str) -> Result, Error> { + if value == "off" { + return Ok(None); + } + let unixtime = parse_unixtime(value)?; + Ok(Some(unixtime)) +} + /// Check whether signatures need to be renewed. /// /// The input is an RRset plus signatures in zonefile format plus a /// duration how long the signatures are required to remain valid. -fn sig_renew(rrset: &[String], remain_time: &Duration) -> bool { +fn sig_renew(rrset: &[String], remain_time: &Duration, now: UnixTime) -> bool { let mut zonefile = Zonefile::new(); for r in rrset { zonefile.extend_from_slice(r.as_ref()); zonefile.extend_from_slice(b"\n"); } - let now = Timestamp::now(); - let renew = now.into_int() as u64 + remain_time.as_secs(); + let now_u64 = Into::::into(now).as_secs(); + let renew = now_u64 + remain_time.as_secs(); for e in zonefile { let e = e.expect("should not fail"); match e { @@ -3960,14 +4191,14 @@ fn make_parent_dir(filename: PathBuf) -> PathBuf { /// Compute when the cron subcommand should be called to refresh signatures /// for an RRset. -fn compute_cron_next(rrset: &[String], remain_time: &Duration) -> Option { +fn compute_cron_next(rrset: &[String], remain_time: &Duration, now: UnixTime) -> Option { let mut zonefile = Zonefile::new(); for r in rrset { zonefile.extend_from_slice(r.as_ref()); zonefile.extend_from_slice(b"\n"); } - let now = SystemTime::now(); + let now_system_time = UNIX_EPOCH + Duration::from(now.clone()); let min_expiration = zonefile .map(|r| r.expect("should not fail")) .filter_map(|r| match r { @@ -3981,7 +4212,7 @@ fn compute_cron_next(rrset: &[String], remain_time: &Duration) -> Option, state_changed: &mut bool, + now: UnixTime, ) -> AutoActionsResult { for a in actions { match a { @@ -4080,7 +4312,7 @@ async fn auto_wait_actions( if let Some(dnskey_status) = &report_state_locked.dnskey { match dnskey_status { AutoReportActionsResult::Wait(next) => { - if *next > UnixTime::now() { + if *next > now { return AutoActionsResult::Wait(next.clone()); } } @@ -4091,7 +4323,7 @@ async fn auto_wait_actions( drop(report_state_locked); } - let result = report_dnskey_propagated(state).await; + let result = report_dnskey_propagated(state, now.clone()).await; let mut report_state_locked = report_state.lock().expect("lock() should not fail"); report_state_locked.dnskey = Some(result.clone()); @@ -4110,7 +4342,7 @@ async fn auto_wait_actions( if let Some(ds_status) = &report_state_locked.ds { match ds_status { AutoReportActionsResult::Wait(next) => { - if *next > UnixTime::now() { + if *next > now { return AutoActionsResult::Wait(next.clone()); } } @@ -4120,10 +4352,12 @@ async fn auto_wait_actions( drop(report_state_locked); } - let result = report_ds_propagated(state).await.unwrap_or_else(|e| { - warn!("Check DS propagation failed: {e}"); - AutoReportActionsResult::Wait(UnixTime::now() + DEFAULT_WAIT) - }); + let result = report_ds_propagated(state, now.clone()) + .await + .unwrap_or_else(|e| { + warn!("Check DS propagation failed: {e}"); + AutoReportActionsResult::Wait(now.clone() + DEFAULT_WAIT) + }); let mut report_state_locked = report_state.lock().expect("lock() should not fail"); report_state_locked.ds = Some(result.clone()); @@ -4149,7 +4383,7 @@ async fn auto_wait_actions( if let Some(rrsig_status) = opt_rrsig_status { match rrsig_status { AutoReportRrsigResult::Wait(next) => { - if next > UnixTime::now() { + if next > now { return AutoActionsResult::Wait(next.clone()); } } @@ -4160,13 +4394,16 @@ async fn auto_wait_actions( ttl, report_ttl, } => { - if next > UnixTime::now() { + if next > now { return AutoActionsResult::Wait(next.clone()); } - let res = check_soa(serial, state).await.unwrap_or_else(|e| { - warn!("Check SOA propagation failed: {e}"); - false - }); + let res = + check_soa(serial, state, now.clone()) + .await + .unwrap_or_else(|e| { + warn!("Check SOA propagation failed: {e}"); + false + }); if res { let mut report_state_locked = report_state.lock().expect("lock() should not fail"); @@ -4176,7 +4413,7 @@ async fn auto_wait_actions( *state_changed = true; continue; } else { - let next = UnixTime::now() + ttl.into(); + let next = now + ttl.into(); let mut report_state_locked = report_state.lock().expect("lock() should not fail"); report_state_locked.rrsig = Some(AutoReportRrsigResult::WaitSoa { @@ -4196,7 +4433,7 @@ async fn auto_wait_actions( rtype, ttl, } => { - if next > UnixTime::now() { + if next > now { return AutoActionsResult::Wait(next.clone()); } let res = @@ -4207,7 +4444,7 @@ async fn auto_wait_actions( false }); if !res { - let next = UnixTime::now() + ttl.into(); + let next = now + ttl.into(); let mut report_state_locked = report_state.lock().expect("lock() should not fail"); report_state_locked.rrsig = @@ -4226,7 +4463,7 @@ async fn auto_wait_actions( // the zone. } AutoReportRrsigResult::WaitNextSerial { next, serial, ttl } => { - if next > UnixTime::now() { + if next > now { return AutoActionsResult::Wait(next.clone()); } let res = check_next_serial(serial, state).await.unwrap_or_else(|e| { @@ -4234,7 +4471,7 @@ async fn auto_wait_actions( false }); if !res { - let next = UnixTime::now() + ttl.into(); + let next = now + ttl.into(); let mut report_state_locked = report_state.lock().expect("lock() should not fail"); report_state_locked.rrsig = @@ -4253,10 +4490,12 @@ async fn auto_wait_actions( } } - let result = report_rrsig_propagated(state).await.unwrap_or_else(|e| { - warn!("Check RRSIG propagation failed: {e}"); - AutoReportRrsigResult::Wait(UnixTime::now() + DEFAULT_WAIT) - }); + let result = report_rrsig_propagated(state, now.clone()) + .await + .unwrap_or_else(|e| { + warn!("Check RRSIG propagation failed: {e}"); + AutoReportRrsigResult::Wait(now.clone() + DEFAULT_WAIT) + }); let mut report_state_locked = report_state.lock().expect("lock() should not fail"); report_state_locked.rrsig = Some(result.clone()); @@ -4291,6 +4530,7 @@ async fn auto_report_actions( kss: &KeySetState, report_state: &Mutex, state_changed: &mut bool, + now: UnixTime, ) -> AutoReportActionsResult { assert!(!actions.is_empty()); let mut max_ttl = Ttl::from_secs(0); @@ -4303,7 +4543,7 @@ async fn auto_report_actions( if let Some(dnskey_status) = &report_state_locked.dnskey { match dnskey_status { AutoReportActionsResult::Wait(next) => { - if *next > UnixTime::now() { + if *next > now { return dnskey_status.clone(); } } @@ -4316,7 +4556,7 @@ async fn auto_report_actions( drop(report_state_locked); } - let result = report_dnskey_propagated(kss).await; + let result = report_dnskey_propagated(kss, now.clone()).await; let mut report_state_locked = report_state.lock().expect("lock() should not fail"); report_state_locked.dnskey = Some(result.clone()); @@ -4337,7 +4577,7 @@ async fn auto_report_actions( if let Some(ds_status) = &report_state_locked.ds { match ds_status { AutoReportActionsResult::Wait(next) => { - if *next > UnixTime::now() { + if *next > now { return ds_status.clone(); } } @@ -4350,10 +4590,12 @@ async fn auto_report_actions( drop(report_state_locked); } - let result = report_ds_propagated(kss).await.unwrap_or_else(|e| { - warn!("Check DS propagation failed: {e}"); - AutoReportActionsResult::Wait(UnixTime::now() + DEFAULT_WAIT) - }); + let result = report_ds_propagated(kss, now.clone()) + .await + .unwrap_or_else(|e| { + warn!("Check DS propagation failed: {e}"); + AutoReportActionsResult::Wait(now.clone() + DEFAULT_WAIT) + }); let mut report_state_locked = report_state.lock().expect("lock() should not fail"); report_state_locked.ds = Some(result.clone()); @@ -4381,7 +4623,7 @@ async fn auto_report_actions( if let Some(rrsig_status) = opt_rrsig_status { match rrsig_status { AutoReportRrsigResult::Wait(next) => { - if next > UnixTime::now() { + if next > now { return AutoReportActionsResult::Wait(next.clone()); } } @@ -4395,13 +4637,16 @@ async fn auto_report_actions( ttl, report_ttl, } => { - if next > UnixTime::now() { + if next > now { return AutoReportActionsResult::Wait(next.clone()); } - let res = check_soa(serial, kss).await.unwrap_or_else(|e| { - warn!("Check SOA propagation failed: {e}"); - false - }); + let res = + check_soa(serial, kss, now.clone()) + .await + .unwrap_or_else(|e| { + warn!("Check SOA propagation failed: {e}"); + false + }); if res { let mut report_state_locked = report_state.lock().expect("lock() should not fail"); @@ -4412,7 +4657,7 @@ async fn auto_report_actions( max_ttl = max(max_ttl, report_ttl); continue; } else { - let next = UnixTime::now() + ttl.into(); + let next = now + ttl.into(); let mut report_state_locked = report_state.lock().expect("lock() should not fail"); report_state_locked.rrsig = Some(AutoReportRrsigResult::WaitSoa { @@ -4432,7 +4677,7 @@ async fn auto_report_actions( rtype, ttl, } => { - if next > UnixTime::now() { + if next > now { return AutoReportActionsResult::Wait(next.clone()); } let res = check_record(&name, &rtype, kss).await.unwrap_or_else(|e| { @@ -4440,7 +4685,7 @@ async fn auto_report_actions( false }); if !res { - let next = UnixTime::now() + ttl.into(); + let next = now + ttl.into(); let mut report_state_locked = report_state.lock().expect("lock() should not fail"); report_state_locked.rrsig = @@ -4459,7 +4704,7 @@ async fn auto_report_actions( // the zone. } AutoReportRrsigResult::WaitNextSerial { next, serial, ttl } => { - if next > UnixTime::now() { + if next > now { return AutoReportActionsResult::Wait(next.clone()); } let res = check_next_serial(serial, kss).await.unwrap_or_else(|e| { @@ -4467,7 +4712,7 @@ async fn auto_report_actions( false }); if !res { - let next = UnixTime::now() + ttl.into(); + let next = now + ttl.into(); let mut report_state_locked = report_state.lock().expect("lock() should not fail"); report_state_locked.rrsig = @@ -4486,10 +4731,12 @@ async fn auto_report_actions( } } - let result = report_rrsig_propagated(kss).await.unwrap_or_else(|e| { - warn!("Check RRSIG propagation failed: {e}"); - AutoReportRrsigResult::Wait(UnixTime::now() + DEFAULT_WAIT) - }); + let result = report_rrsig_propagated(kss, now.clone()) + .await + .unwrap_or_else(|e| { + warn!("Check RRSIG propagation failed: {e}"); + AutoReportRrsigResult::Wait(now.clone() + DEFAULT_WAIT) + }); let mut report_state_locked = report_state.lock().expect("lock() should not fail"); report_state_locked.rrsig = Some(result.clone()); @@ -4525,75 +4772,12 @@ async fn auto_report_actions( AutoReportActionsResult::Report(max_ttl) } -/// Check whether automatic actions are done or not. If not, return until -/// when to wait to try again. -fn check_auto_actions(actions: &[Action], report_state: &Mutex) -> AutoActionsResult { - for a in actions { - match a { - Action::UpdateDnskeyRrset - | Action::CreateCdsRrset - | Action::RemoveCdsRrset - | Action::UpdateDsRrset - | Action::UpdateRrsig => (), - Action::ReportDnskeyPropagated | Action::WaitDnskeyPropagated => { - let report_state_locked = report_state.lock().expect("lock() should not fail"); - if let Some(dnskey_status) = &report_state_locked.dnskey { - match dnskey_status { - AutoReportActionsResult::Wait(next) => { - return AutoActionsResult::Wait(next.clone()) - } - AutoReportActionsResult::Report(_) => continue, - } - } - drop(report_state_locked); - - // No status, request cron - return AutoActionsResult::Wait(UnixTime::now()); - } - Action::ReportDsPropagated | Action::WaitDsPropagated => { - let report_state_locked = report_state.lock().expect("lock() should not fail"); - if let Some(ds_status) = &report_state_locked.ds { - match ds_status { - AutoReportActionsResult::Wait(next) => { - return AutoActionsResult::Wait(next.clone()) - } - AutoReportActionsResult::Report(_) => continue, - } - } - drop(report_state_locked); - - // No status, request cron - return AutoActionsResult::Wait(UnixTime::now()); - } - Action::ReportRrsigPropagated | Action::WaitRrsigPropagated => { - let report_state_locked = report_state.lock().expect("lock() should not fail"); - if let Some(rrsig_status) = &report_state_locked.rrsig { - match rrsig_status { - AutoReportRrsigResult::Wait(next) - | AutoReportRrsigResult::WaitRecord { next, .. } - | AutoReportRrsigResult::WaitNextSerial { next, .. } - | AutoReportRrsigResult::WaitSoa { next, .. } => { - return AutoActionsResult::Wait(next.clone()) - } - AutoReportRrsigResult::Report(_) => continue, - } - } - drop(report_state_locked); - - // No status, request cron - return AutoActionsResult::Wait(UnixTime::now()); - } - } - } - AutoActionsResult::Ok -} - /// Check whether a new DNSKEY RRset has propagated. /// /// Compile a list of nameservers for the zone and their addresses and /// query each address for the DNSKEY RRset. The function /// check_dnskey_for_address does the actual work. -async fn report_dnskey_propagated(kss: &KeySetState) -> AutoReportActionsResult { +async fn report_dnskey_propagated(kss: &KeySetState, now: UnixTime) -> AutoReportActionsResult { // Convert the DNSKEY RRset plus RRSIGs into a HashSet. // Find the address of all name servers of zone // Ask each nameserver for the DNSKEY RRset. Check if it matches the @@ -4615,7 +4799,7 @@ async fn report_dnskey_propagated(kss: &KeySetState) -> AutoReportActionsResult Ok(a) => a, Err(e) => { warn!("Getting nameserver addresses for {zone} failed: {e}"); - return AutoReportActionsResult::Wait(UnixTime::now() + DEFAULT_WAIT); + return AutoReportActionsResult::Wait(now + DEFAULT_WAIT); } }; @@ -4624,7 +4808,7 @@ async fn report_dnskey_propagated(kss: &KeySetState) -> AutoReportActionsResult let futures: Vec<_> = addresses .iter() - .map(|a| check_dnskey_for_address(zone, a, target_dnskey.clone())) + .map(|a| check_dnskey_for_address(zone, a, target_dnskey.clone(), now.clone())) .collect(); let res: Vec<_> = join_all(futures).await; @@ -4637,7 +4821,7 @@ async fn report_dnskey_propagated(kss: &KeySetState) -> AutoReportActionsResult Ok(r) => r, Err(e) => { warn!("DNSKEY check failed: {e}"); - return AutoReportActionsResult::Wait(UnixTime::now() + DEFAULT_WAIT); + return AutoReportActionsResult::Wait(now + DEFAULT_WAIT); } }; match r { @@ -4662,7 +4846,10 @@ async fn report_dnskey_propagated(kss: &KeySetState) -> AutoReportActionsResult /// query each address for the DS RRset. The function /// check_ds_for_address does the actual work. The CDNSKEY RRset is /// used as the reference for the DS RRset. -async fn report_ds_propagated(kss: &KeySetState) -> Result { +async fn report_ds_propagated( + kss: &KeySetState, + now: UnixTime, +) -> Result { // Convert the CDNSKEY RRset into a HashSet. // Find the name of the parent zone. // Find the address of all name servers of the parent zone. @@ -4700,7 +4887,7 @@ async fn report_ds_propagated(kss: &KeySetState) -> Result = addresses .iter() - .map(|a| check_ds_for_address(zone, a, target_dnskey.clone())) + .map(|a| check_ds_for_address(zone, a, target_dnskey.clone(), now.clone())) .collect(); let res: Vec<_> = join_all(futures).await; let mut max_ttl = None; @@ -4732,7 +4919,10 @@ async fn report_ds_propagated(kss: &KeySetState) -> Result Result { +async fn report_rrsig_propagated( + kss: &KeySetState, + now: UnixTime, +) -> Result { // This function assume a single signer. Multi-signer is not supported // at all, but any kind of active-passive or active-active setup would also // need changes. With more than one signer, each signer needs to be @@ -4741,7 +4931,7 @@ async fn report_rrsig_propagated(kss: &KeySetState) -> Result unreachable!(), @@ -4757,14 +4947,17 @@ async fn report_rrsig_propagated(kss: &KeySetState) -> Result Result Result { +async fn check_zone(kss: &KeySetState, now: UnixTime) -> Result { let expected_set = get_expected_zsk_key_tags(kss); let zone = kss.keyset.name(); @@ -4875,14 +5068,14 @@ async fn check_zone(kss: &KeySetState) -> Result { let res = check_rrsigs(treemap, sigmap, zone, expected_set); return match res { CheckRrsigsResult::Done => Ok(AutoReportRrsigResult::WaitSoa { - next: UnixTime::now(), + next: now, serial, ttl: r.ttl(), report_ttl: max_ttl, }), CheckRrsigsResult::WaitRecord { name, rtype } => { Ok(AutoReportRrsigResult::WaitRecord { - next: UnixTime::now() + r.ttl().into(), + next: now + r.ttl().into(), name, rtype, ttl: r.ttl(), @@ -4890,7 +5083,7 @@ async fn check_zone(kss: &KeySetState) -> Result { } CheckRrsigsResult::WaitNextSerial => { Ok(AutoReportRrsigResult::WaitNextSerial { - next: UnixTime::now() + r.ttl().into(), + next: now + r.ttl().into(), serial, ttl: r.ttl(), }) @@ -4999,6 +5192,7 @@ async fn check_dnskey_for_address( zone: &Name>, address: &IpAddr, mut target_dnskey: HashSet, + now: UnixTime, ) -> Result { let records = lookup_name_rtype_at_address(zone, Rtype::DNSKEY, address).await?; @@ -5026,9 +5220,7 @@ async fn check_dnskey_for_address( // The current record is not found in the target set. Wait // until the TTL has expired. debug!("Check DNSKEY RRset: DNSKEY record not expected"); - return Ok(AutoReportActionsResult::Wait( - UnixTime::now() + r.ttl().into_duration(), - )); + return Ok(AutoReportActionsResult::Wait(now + r.ttl().into_duration())); } continue; } @@ -5053,9 +5245,7 @@ async fn check_dnskey_for_address( // The current record is not found in the target set. Wait // until the TTL has expired. debug!("Check DNSKEY RRset: RRSIG record not expected"); - return Ok(AutoReportActionsResult::Wait( - UnixTime::now() + r.ttl().into_duration(), - )); + return Ok(AutoReportActionsResult::Wait(now + r.ttl().into_duration())); } continue; } @@ -5063,9 +5253,7 @@ async fn check_dnskey_for_address( if let Some(record) = target_dnskey.iter().next() { // Not all DNSKEY records were found. warn!("Not all required DNSKEY records were found for {zone}"); - Ok(AutoReportActionsResult::Wait( - UnixTime::now() + record.ttl().into(), - )) + Ok(AutoReportActionsResult::Wait(now + record.ttl().into())) } else { Ok(AutoReportActionsResult::Report(max_ttl)) } @@ -5076,6 +5264,7 @@ async fn check_ds_for_address( zone: &Name>, address: &IpAddr, mut target_dnskey: HashSet, + now: UnixTime, ) -> Result { let records = lookup_name_rtype_at_address::>(zone, Rtype::DS, address).await?; @@ -5103,9 +5292,7 @@ async fn check_ds_for_address( // The current record is not found in the target set. Wait // until the TTL has expired. debug!("Check DS RRset: DS record not expected"); - return Ok(AutoReportActionsResult::Wait( - UnixTime::now() + r.ttl().into_duration(), - )); + return Ok(AutoReportActionsResult::Wait(now + r.ttl().into_duration())); } continue; } @@ -5113,9 +5300,7 @@ async fn check_ds_for_address( if let Some(dnskey) = dnskey { debug!("Check DS RRset: expected DS record not present"); let ttl = dnskey.ttl(); - Ok(AutoReportActionsResult::Wait( - UnixTime::now() + ttl.into_duration(), - )) + Ok(AutoReportActionsResult::Wait(now + ttl.into_duration())) } else { Ok(AutoReportActionsResult::Report(max_ttl)) } @@ -5127,13 +5312,12 @@ async fn check_soa_for_address( zone: &Name>, address: &IpAddr, serial: Serial, + now: UnixTime, ) -> Result { let records = lookup_name_rtype_at_address::>(zone, Rtype::SOA, address).await?; if records.is_empty() { - return Ok(AutoReportActionsResult::Wait( - UnixTime::now() + DEFAULT_WAIT, - )); + return Ok(AutoReportActionsResult::Wait(now + DEFAULT_WAIT)); } if let Some(ttl) = records @@ -5147,7 +5331,7 @@ async fn check_soa_for_address( }) .next() { - return Ok(AutoReportActionsResult::Wait(UnixTime::now() + ttl.into())); + return Ok(AutoReportActionsResult::Wait(now + ttl.into())); } // Return a dummy TTL. The caller knows the real TTL to report. Ok(AutoReportActionsResult::Report(Ttl::from_secs(0))) @@ -5276,91 +5460,6 @@ fn cron_next_auto_start( } } -/// This function computes when next to try to move to the next state. -/// -/// For the Report and Wait actions that involves checking when propagation -/// should be tested again. For the expire step it computes when the -/// keyset object in the domain library accepts the cache_expired1 or -/// cache_expired2 methods. -fn cron_next_auto_report_expire_done( - auto: &AutoConfig, - roll_list: &[RollType], - kss: &KeySetState, - cron_next: &mut Vec>, -) -> Result<(), Error> { - if auto.report { - // If there is currently a roll in one of the propagation - // states and this flags is set take when to check again for - // actions to complete - for r in roll_list { - if let Some(state) = kss.keyset.rollstates().get(r) { - let report_state = kss.internal.get(r).expect("should not fail"); - let report_state = match state { - RollState::Propagation1 => &report_state.propagation1, - RollState::Propagation2 => &report_state.propagation2, - _ => continue, - }; - let actions = kss.keyset.actions(*r); - match check_auto_actions(&actions, report_state) { - AutoActionsResult::Ok => { - // All actions are ready. Request cron. - cron_next.push(Some(UnixTime::now())); - } - AutoActionsResult::Wait(next) => cron_next.push(Some(next)), - } - } - } - } - - if auto.expire { - // If there is currently a roll in one of the cache expire - // states and this flag is set, use the remaining time until caches - // are expired. Try to issue the cache_expire[12] method on a - // clone of keyset. - let mut keyset = kss.keyset.clone(); - for r in roll_list { - if let Some(state) = keyset.rollstates().get(r) { - let actions = match state { - RollState::CacheExpire1(_) => keyset.cache_expired1(*r), - RollState::CacheExpire2(_) => keyset.cache_expired2(*r), - _ => continue, - }; - if let Err(keyset::Error::Wait(remain)) = actions { - cron_next.push(Some(UnixTime::now() + remain)); - continue; - } - let _ = actions - .map_err(|e| format!("cache_expired[12] failed for state {r:?}: {e}"))?; - - // Time to call cron. Report the current time. - cron_next.push(Some(UnixTime::now())); - } - } - } - - if auto.done { - // If there is current a roll in the done state and all - // and this flag is set, take when the check again for actions to - // complete - for r in roll_list { - if let Some(RollState::Done) = kss.keyset.rollstates().get(r) { - let report_state = kss.internal.get(r).expect("should not fail"); - match check_auto_actions(&kss.keyset.actions(*r), &report_state.done) { - AutoActionsResult::Ok => { - // All actions are ready. Request cron. - cron_next.push(Some(UnixTime::now())); - } - AutoActionsResult::Wait(next) => { - cron_next.push(Some(next)); - } - } - } - } - } - - Ok(()) -} - /// The result of checking whether all RRSIG records are present. #[derive(PartialEq)] enum CheckRrsigsResult { @@ -5542,7 +5641,7 @@ async fn check_next_serial(serial: Serial, kss: &KeySetState) -> Result Result { +async fn check_soa(serial: Serial, kss: &KeySetState, now: UnixTime) -> Result { // Find the address of all name servers of zone // Ask each nameserver for the SOA record. // Check that it's version is at least the version we checked. @@ -5554,7 +5653,7 @@ async fn check_soa(serial: Serial, kss: &KeySetState) -> Result { let addresses = addresses_for_zone(zone).await?; let futures: Vec<_> = addresses .iter() - .map(|a| check_soa_for_address(zone, a, serial)) + .map(|a| check_soa_for_address(zone, a, serial, now.clone())) .collect(); let res: Vec<_> = join_all(futures).await;