From 9aa25ec484e8c07bfc3ad0b485d8e93e2e8106ea Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:46:06 +0200 Subject: [PATCH 01/15] Initial support for `set tsig-store-path` and `set publication-nameserver`. To support use of TSIG in Cascade. Lacks RustDocs, man page entries and additional logging. --- src/commands/keyset/cmd.rs | 163 ++++++++++++++++++++++++++++++++---- src/commands/keyset/mod.rs | 2 + src/commands/keyset/tsig.rs | 79 +++++++++++++++++ 3 files changed, 228 insertions(+), 16 deletions(-) create mode 100644 src/commands/keyset/tsig.rs diff --git a/src/commands/keyset/cmd.rs b/src/commands/keyset/cmd.rs index 0d77363e..5cffb0a9 100644 --- a/src/commands/keyset/cmd.rs +++ b/src/commands/keyset/cmd.rs @@ -2,6 +2,7 @@ #![warn(missing_docs)] #![warn(clippy::missing_docs_in_private_items)] +use crate::commands::keyset::tsig::TsigKeyStore; use crate::env::Env; use crate::error::Error; use crate::util; @@ -31,6 +32,8 @@ use domain::net::client::request::{ ComposeRequest, RequestMessage, RequestMessageMulti, SendRequest, SendRequestMulti, }; use domain::net::client::stream; +use domain::net::client::tsig::Connection as TsigConnection; +use domain::net::client::tsig::RequestMessage as TsigRequestMessage; use domain::rdata::dnssec::Timestamp; use domain::rdata::{AllRecordData, Cdnskey, Cds, Dnskey, Ds, Rrsig, Soa, ZoneRecordData}; use domain::resolv::lookup::lookup_host; @@ -60,7 +63,7 @@ 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::sync::{Arc, Mutex}; use std::time::{Duration, UNIX_EPOCH}; use tokio::net::TcpStream; #[cfg(feature = "kmip")] @@ -501,6 +504,26 @@ enum SetCommands { #[arg(value_parser = parse_opt_unixtime)] opt_unixtime: OptUnixTime, }, + + /// Set the location of the TSIG store to use to retrieve TSIG secrets + /// when needed. + TsigStorePath { + /// The path to the TSIG store file. + path: PathBuf, + }, + + /// Specify a nameserver to request XFR from. If not specified the + /// SOA MNAME nameserver will be used. + PublicationNameserver { + /// The address and port number of the nameserver. + addr: SocketAddr, + + /// Optional TSIG key to use when communicating with the nameserver, + /// + /// TsigStorePath must also have been provided and the specified + /// store must contain a key by this name. + tsig_key_name: Option, + }, } /// The various subcommands of a key roll command. @@ -673,6 +696,9 @@ struct WorkSpace { #[cfg(feature = "kmip")] /// The current set of KMIP server pools. pools: HashMap, + + /// A store of TSIG keys indexed by key name. + tsig_store: TsigKeyStore, } impl Keyset { @@ -739,6 +765,8 @@ impl Keyset { autoremove_delay: DEFAULT_AUTOREMOVE_DELAY, update_ds_command: Vec::new(), faketime: None, + tsig_store_path: None, + nameservers: HashSet::new(), }; // Create the parent directies. @@ -764,6 +792,7 @@ impl Keyset { _locked_config_file: None, #[cfg(feature = "kmip")] pools: HashMap::new(), + tsig_store: TsigKeyStore::new(), }; ws.write_state()?; @@ -785,6 +814,15 @@ impl Keyset { let kss: KeySetState = serde_json::from_reader(file) .map_err(|e| format!("error loading {:?}: {e}\n", ksc.state_file))?; + let tsig_store = if let Some(path) = &ksc.tsig_store_path { + let store_file = file_with_write_lock(path)?; + let store: TsigKeyStore = serde_json::from_reader(&store_file) + .map_err(|e| format!("error loading {}: {e}\n", path.display()))?; + store + } else { + TsigKeyStore::new() + }; + let mut ws = WorkSpace { config: ksc, state: kss, @@ -794,6 +832,7 @@ impl Keyset { _locked_config_file: Some(config_file), #[cfg(feature = "kmip")] pools: HashMap::new(), + tsig_store, }; let now = ws.faketime_or_now(); @@ -1729,6 +1768,13 @@ struct KeySetConfig { /// /// This is needed for integration tests. faketime: Option, + + /// Path to TSIG secret store to lookup TSIG secrets when needed. + tsig_store_path: Option, + + /// Optional nameservers to request XFR from instead of the SOA MNAME + /// defined nameserver. + nameservers: HashSet, } /// Configuration for key roll automation. @@ -1816,6 +1862,17 @@ impl Display for ZskRollType { } } +/// Details needed to connect to a nameserver. +#[derive(Debug, Deserialize, Serialize, Hash, PartialEq, Eq)] +pub struct NameserverConnectionDetails { + /// The address and port number at which this nameserver accepts DNS + /// requests. + pub addr: SocketAddr, + + /// Optional TSIG key to use when communicating with this nameserver. + pub tsig_key_name: Option, +} + /// Persistent state for the keyset command. #[derive(Deserialize, Serialize)] pub struct KeySetState { @@ -2151,7 +2208,21 @@ impl WorkSpace { SetCommands::UpdateDsCommand { args } => { self.config.update_ds_command = args; } - SetCommands::FakeTime { opt_unixtime } => self.config.faketime = opt_unixtime, + SetCommands::FakeTime { opt_unixtime } => { + self.config.faketime = opt_unixtime; + } + SetCommands::TsigStorePath { path } => { + self.config.tsig_store_path = Some(path); + } + SetCommands::PublicationNameserver { + addr, + tsig_key_name, + } => { + self.config.nameservers.insert(NameserverConnectionDetails { + addr, + tsig_key_name, + }); + } } self.config_changed = true; Ok(()) @@ -3790,6 +3861,8 @@ impl WorkSpace { report_state, &mut self.state_changed, now.clone(), + &self.config.nameservers, + &self.tsig_store, ) .await { @@ -3858,6 +3931,8 @@ impl WorkSpace { report_state, &mut self.state_changed, now.clone(), + &self.config.nameservers, + &self.tsig_store, ) .await { @@ -4408,6 +4483,8 @@ async fn auto_wait_actions( report_state: &Mutex, state_changed: &mut bool, now: UnixTime, + nameservers: &HashSet, + tsig_store: &TsigKeyStore, ) -> AutoActionsResult { for a in actions { match a { @@ -4602,7 +4679,7 @@ async fn auto_wait_actions( } } - let result = report_rrsig_propagated(state, now.clone()) + let result = report_rrsig_propagated(state, now.clone(), nameservers, tsig_store) .await .unwrap_or_else(|e| { warn!("Check RRSIG propagation failed: {e}"); @@ -4643,6 +4720,8 @@ async fn auto_report_actions( report_state: &Mutex, state_changed: &mut bool, now: UnixTime, + nameservers: &HashSet, + tsig_store: &TsigKeyStore, ) -> AutoReportActionsResult { assert!(!actions.is_empty()); let mut max_ttl = Ttl::from_secs(0); @@ -4843,7 +4922,7 @@ async fn auto_report_actions( } } - let result = report_rrsig_propagated(kss, now.clone()) + let result = report_rrsig_propagated(kss, now.clone(), nameservers, tsig_store) .await .unwrap_or_else(|e| { warn!("Check RRSIG propagation failed: {e}"); @@ -5034,6 +5113,8 @@ async fn report_ds_propagated( async fn report_rrsig_propagated( kss: &KeySetState, now: UnixTime, + nameservers: &HashSet, + tsig_store: &TsigKeyStore, ) -> 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 @@ -5043,7 +5124,7 @@ async fn report_rrsig_propagated( // Check the zone. If the zone checks out, make sure that all nameservers // have at least the version of the zone that was checked. - let result = check_zone(kss, now.clone()).await?; + let result = check_zone(kss, now.clone(), nameservers, tsig_store).await?; let (serial, ttl, report_ttl) = match result { // check_zone never returns Report or Wait. AutoReportRrsigResult::Report(_) | AutoReportRrsigResult::Wait(_) => unreachable!(), @@ -5091,7 +5172,12 @@ async fn report_rrsig_propagated( /// a HashSet of type as the value. Check that each name and type has a /// corresponding complete RRSIG set. /// Ignore delegated records -async fn check_zone(kss: &KeySetState, now: UnixTime) -> Result { +async fn check_zone( + kss: &KeySetState, + now: UnixTime, + nameservers: &HashSet, + tsig_store: &TsigKeyStore, +) -> Result { let expected_set = get_expected_zsk_key_tags(kss); let zone = kss.keyset.name(); @@ -5115,19 +5201,64 @@ async fn check_zone(kss: &KeySetState, now: UnixTime) -> Result; + let nameservers = if !nameservers.is_empty() { + nameservers + } else { + mname_nameservers = addresses_for_name(&resolver, mname) + .await? + .iter() + .map(|ip| NameserverConnectionDetails { + addr: SocketAddr::new(*ip, 53), + tsig_key_name: None, + }) + .collect(); + &mname_nameservers + }; - 'addr: for a in &addresses { - let tcp_conn = match TcpStream::connect((*a, 53_u16)).await { + 'addr: for ns in nameservers { + let tcp_conn = match TcpStream::connect(ns.addr).await { Ok(conn) => conn, Err(e) => { - warn!("DNS TCP connection to {a} failed: {e}"); + warn!("DNS TCP connection to {} failed: {e}", ns.addr); continue; } }; - let (tcp, transport) = stream::Connection::>, _>::new(tcp_conn); - tokio::spawn(transport.run()); + // Prepare the named TSIG key for use, if any. + let tsig_key = ns.tsig_key_name.as_ref().and_then(|name| { + match Name::from_str(name) { + Ok(name) => { + tsig_store.map.get(&name).and_then(|key| { + domain::tsig::Key::new(key.alg.into(), &key.data, name, None, None) + .inspect_err(|_err| { /* TODO: Log error */ }) + .ok() + }) + } + Err(_err) => None, // TODO: Log error + } + }); + + // If we have a TSIG key, setup a TSIG capable transport, otherwise + // use a normal transport. Use Multi types because only those support + // the multiple possible responses that can occur when sending an + // XFR request. + let tcp: Box>>> = if let Some(tsig_key) = + tsig_key + { + let (conn, transport) = stream::Connection::< + TsigRequestMessage>, Arc>, + _, + >::new(tcp_conn); + tokio::spawn(transport.run()); + Box::new(TsigConnection::new(tsig_key, conn)) + } else { + let (conn, transport) = stream::Connection::>, _>::new(tcp_conn); + tokio::spawn(transport.run()); + Box::new(conn) + }; let msg = MessageBuilder::new_vec(); let mut msg = msg.question(); @@ -5135,7 +5266,7 @@ async fn check_zone(kss: &KeySetState, now: UnixTime) -> Result Result reply, Err(e) => { - warn!("reading AXFR response from {a} failed: {e}"); + warn!("reading AXFR response from {} failed: {e}", ns.addr); continue 'addr; } }; @@ -5156,7 +5287,7 @@ async fn check_zone(kss: &KeySetState, now: UnixTime) -> Result Result for domain::tsig::Algorithm { + fn from(alg: AlgSpec) -> Self { + match alg { + AlgSpec::Sha1 => domain::tsig::Algorithm::Sha1, + AlgSpec::Sha256 => domain::tsig::Algorithm::Sha256, + AlgSpec::Sha384 => domain::tsig::Algorithm::Sha384, + AlgSpec::Sha512 => domain::tsig::Algorithm::Sha512, + } + } +} + +/// A TSIG key. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct KeySpec { + /// The key algorithm. + pub alg: AlgSpec, + + /// The private key material. + pub data: Box<[u8]>, +} + +pub type TsigKeyName = Name>; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct TsigKeyStore { + /// The data format version of the store file. + pub version: String, + + /// A mapping of names to TSIG key details. + pub map: HashMap, +} + +impl TsigKeyStore { + pub fn new() -> Self { + Self { + version: "v1".to_string(), + map: HashMap::new() + } + } + + pub fn get(&self, name: &TsigKeyName) -> Result, String> { + if let Some(key) = self.map.get(name) { + domain::tsig::Key::new(key.alg.into(), &key.data, name.to_owned(), None, None) + .map(Option::Some) + .map_err(|err| err.to_string()) + } else { + Ok(None) + } + } +} + +impl Default for TsigKeyStore { + fn default() -> Self { + Self::new() + } +} From 9dd9dfdc3f66a469ce7f870231c91111d5eabf44 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:49:00 +0200 Subject: [PATCH 02/15] cargo fmt. --- src/commands/keyset/tsig.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/keyset/tsig.rs b/src/commands/keyset/tsig.rs index ec525f29..1d77e409 100644 --- a/src/commands/keyset/tsig.rs +++ b/src/commands/keyset/tsig.rs @@ -57,7 +57,7 @@ impl TsigKeyStore { pub fn new() -> Self { Self { version: "v1".to_string(), - map: HashMap::new() + map: HashMap::new(), } } From 6daa3a220b666a73fb82e2876f6e3e50618c62ec Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:56:10 +0200 Subject: [PATCH 03/15] Add From<&IpAddr> for NameserverConnectionDetails. --- src/commands/keyset/cmd.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/commands/keyset/cmd.rs b/src/commands/keyset/cmd.rs index 5cffb0a9..c09600aa 100644 --- a/src/commands/keyset/cmd.rs +++ b/src/commands/keyset/cmd.rs @@ -1873,6 +1873,15 @@ pub struct NameserverConnectionDetails { pub tsig_key_name: Option, } +impl From<&IpAddr> for NameserverConnectionDetails { + fn from(ip: &IpAddr) -> Self { + Self { + addr: SocketAddr::new(*ip, 53), + tsig_key_name: None, + } + } +} + /// Persistent state for the keyset command. #[derive(Deserialize, Serialize)] pub struct KeySetState { @@ -5210,10 +5219,7 @@ async fn check_zone( mname_nameservers = addresses_for_name(&resolver, mname) .await? .iter() - .map(|ip| NameserverConnectionDetails { - addr: SocketAddr::new(*ip, 53), - tsig_key_name: None, - }) + .map(Into::into) .collect(); &mname_nameservers }; From f0348afa71768c7ebdf375f5815457d2b612aee8 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:15:12 +0200 Subject: [PATCH 04/15] (De)serialize to Base64. As Base64 is typically how tooling present/accept TSIG key secret data to/from users. Requires the same change to be made to Cascade too. --- src/commands/keyset/tsig.rs | 39 +++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/commands/keyset/tsig.rs b/src/commands/keyset/tsig.rs index 1d77e409..d948c293 100644 --- a/src/commands/keyset/tsig.rs +++ b/src/commands/keyset/tsig.rs @@ -7,25 +7,25 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "kebab-case")] pub enum AlgSpec { /// SHA-1. - Sha1, + HmacSha1, /// SHA-256. - Sha256, + HmacSha256, /// SHA-384, - Sha384, + HmacSha384, /// SHA-512. - Sha512, + HmacSha512, } impl From for domain::tsig::Algorithm { fn from(alg: AlgSpec) -> Self { match alg { - AlgSpec::Sha1 => domain::tsig::Algorithm::Sha1, - AlgSpec::Sha256 => domain::tsig::Algorithm::Sha256, - AlgSpec::Sha384 => domain::tsig::Algorithm::Sha384, - AlgSpec::Sha512 => domain::tsig::Algorithm::Sha512, + AlgSpec::HmacSha1 => domain::tsig::Algorithm::Sha1, + AlgSpec::HmacSha256 => domain::tsig::Algorithm::Sha256, + AlgSpec::HmacSha384 => domain::tsig::Algorithm::Sha384, + AlgSpec::HmacSha512 => domain::tsig::Algorithm::Sha512, } } } @@ -38,9 +38,32 @@ pub struct KeySpec { pub alg: AlgSpec, /// The private key material. + #[serde(with = "tsig_base64")] pub data: Box<[u8]>, } +mod tsig_base64 { + use domain::utils::base64; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize(data: &[u8], serializer: S) -> Result + where + S: Serializer, + { + base64::encode_string(data).serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let data = base64::decode::>(&s) + .map_err(serde::de::Error::custom)?; + Ok(data.into()) + } +} + pub type TsigKeyName = Name>; #[derive(Clone, Debug, Serialize, Deserialize)] From 3c70885045ecb0442e0136fbcf42521f0d7935c3 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:18:33 +0200 Subject: [PATCH 05/15] cargo fmt. --- src/commands/keyset/tsig.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/keyset/tsig.rs b/src/commands/keyset/tsig.rs index d948c293..0907c76c 100644 --- a/src/commands/keyset/tsig.rs +++ b/src/commands/keyset/tsig.rs @@ -58,8 +58,7 @@ mod tsig_base64 { D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; - let data = base64::decode::>(&s) - .map_err(serde::de::Error::custom)?; + let data = base64::decode::>(&s).map_err(serde::de::Error::custom)?; Ok(data.into()) } } From a6da3d501dbb620810fd90b7754ddca3cedf650d Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Tue, 7 Apr 2026 16:12:32 +0200 Subject: [PATCH 06/15] Make it possible to remove the TSIG store. Set publication-nameservers takes a list of nameservers. Missing bits: checks when remove a TSIG store, checks when adding nameservers. --- src/commands/keyset/cmd.rs | 89 +++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/src/commands/keyset/cmd.rs b/src/commands/keyset/cmd.rs index c09600aa..29f4fbc7 100644 --- a/src/commands/keyset/cmd.rs +++ b/src/commands/keyset/cmd.rs @@ -171,6 +171,10 @@ type OptDuration = Option; /// treats Option special. type OptUnixTime = Option; +/// Type for an optional path name. A separate type is needed because CLAP +/// treats Option special. +type OptPathBuf = Option; + /// The subcommands of the keyset utility. #[allow(clippy::large_enum_variant)] #[derive(Clone, Debug, Subcommand)] @@ -497,33 +501,34 @@ enum SetCommands { 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, - }, - /// Set the location of the TSIG store to use to retrieve TSIG secrets /// when needed. TsigStorePath { /// The path to the TSIG store file. - path: PathBuf, + #[arg(value_parser = parse_opt_pathbuf)] + opt_path: OptPathBuf, }, /// Specify a nameserver to request XFR from. If not specified the /// SOA MNAME nameserver will be used. - PublicationNameserver { + PublicationNameservers { /// The address and port number of the nameserver. - addr: SocketAddr, - - /// Optional TSIG key to use when communicating with the nameserver, + /// Optionally followed by the TSIG key name to use. The TSIG key + /// name is preceded by a caret (^) character. /// /// TsigStorePath must also have been provided and the specified /// store must contain a key by this name. - tsig_key_name: Option, + addrs: 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. @@ -1882,6 +1887,27 @@ impl From<&IpAddr> for NameserverConnectionDetails { } } +impl TryFrom<&str> for NameserverConnectionDetails { + type Error = Error; + + // Note: this only accepts IP addresses, not hostnames. In addition, + // a port is required, there is no default port. TODO: allow hostnames + // and allow the port to be optional. + fn try_from(s: &str) -> Result { + let mut iter = s.split('!'); + let Some(addr_port) = iter.next() else { + return Err("Address expected".into()); + }; + let addr = addr_port.parse() + .map_err(|e| format!("unable to parse address {addr_port}: {e}"))?; + let tsig_key_name = iter.next().map(|v| v.to_string()); + Ok(Self { + addr, + tsig_key_name, + }) + } +} + /// Persistent state for the keyset command. #[derive(Deserialize, Serialize)] pub struct KeySetState { @@ -2220,17 +2246,20 @@ impl WorkSpace { SetCommands::FakeTime { opt_unixtime } => { self.config.faketime = opt_unixtime; } - SetCommands::TsigStorePath { path } => { - self.config.tsig_store_path = Some(path); - } - SetCommands::PublicationNameserver { - addr, - tsig_key_name, - } => { - self.config.nameservers.insert(NameserverConnectionDetails { - addr, - tsig_key_name, - }); + SetCommands::TsigStorePath { opt_path } => { + // TODO: when removing the TSIG store, check that there are + // no publication nameservers that reference the store. + self.config.tsig_store_path = opt_path; + } + SetCommands::PublicationNameservers { addrs } => { + self.config.nameservers = HashSet::new(); + for a in addrs { + // When adding nameservers, check that referenced TSIG + // keys are in the TSIG store. + self.config + .nameservers + .insert(NameserverConnectionDetails::try_from(a.as_str())?); + } } } self.config_changed = true; @@ -4327,6 +4356,16 @@ fn parse_opt_unixtime(value: &str) -> Result, Error> { Ok(Some(unixtime)) } +/// Parse an optional PathBuf from a string but also allow 'off' to signal +/// no PathBuf. +fn parse_opt_pathbuf(value: &str) -> Result, Error> { + if value == "off" { + return Ok(None); + } + let path_buf = PathBuf::from(value); + Ok(Some(path_buf)) +} + /// Check whether signatures need to be renewed. /// /// The input is an RRset plus signatures in zonefile format plus a From bff762297889a917ea4a0af23f3a2048a8672f4e Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Tue, 7 Apr 2026 16:18:50 +0200 Subject: [PATCH 07/15] Fmt. --- src/commands/keyset/cmd.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/commands/keyset/cmd.rs b/src/commands/keyset/cmd.rs index 29f4fbc7..be108b1e 100644 --- a/src/commands/keyset/cmd.rs +++ b/src/commands/keyset/cmd.rs @@ -528,7 +528,6 @@ enum SetCommands { #[arg(value_parser = parse_opt_unixtime)] opt_unixtime: OptUnixTime, }, - } /// The various subcommands of a key roll command. @@ -1898,7 +1897,8 @@ impl TryFrom<&str> for NameserverConnectionDetails { let Some(addr_port) = iter.next() else { return Err("Address expected".into()); }; - let addr = addr_port.parse() + let addr = addr_port + .parse() .map_err(|e| format!("unable to parse address {addr_port}: {e}"))?; let tsig_key_name = iter.next().map(|v| v.to_string()); Ok(Self { @@ -2247,15 +2247,15 @@ impl WorkSpace { self.config.faketime = opt_unixtime; } SetCommands::TsigStorePath { opt_path } => { - // TODO: when removing the TSIG store, check that there are - // no publication nameservers that reference the store. + // TODO: when removing the TSIG store, check that there are + // no publication nameservers that reference the store. self.config.tsig_store_path = opt_path; } SetCommands::PublicationNameservers { addrs } => { self.config.nameservers = HashSet::new(); for a in addrs { - // When adding nameservers, check that referenced TSIG - // keys are in the TSIG store. + // When adding nameservers, check that referenced TSIG + // keys are in the TSIG store. self.config .nameservers .insert(NameserverConnectionDetails::try_from(a.as_str())?); From 0dfe145eca88a9de20f4e1c607bd60e44133c4c3 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Tue, 7 Apr 2026 19:18:00 +0200 Subject: [PATCH 08/15] Make nameservers optional. --- src/commands/keyset/cmd.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/keyset/cmd.rs b/src/commands/keyset/cmd.rs index be108b1e..d3a10086 100644 --- a/src/commands/keyset/cmd.rs +++ b/src/commands/keyset/cmd.rs @@ -1778,6 +1778,7 @@ struct KeySetConfig { /// Optional nameservers to request XFR from instead of the SOA MNAME /// defined nameserver. + #[serde(default)] nameservers: HashSet, } From de33989ec732add47dcdfcbb6087e4c90bfc9541 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:34:38 +0200 Subject: [PATCH 09/15] Various improvements. - Remove unnecessary Result. - Enforce v1 when parsing a persisted key store. - Fail earlier on key name parsing. - Verify that referenced TSIG keys exist in the store. - Include 'hmac-' in the (de)serialized TSIG algorithm name. --- src/commands/keyset/cmd.rs | 69 +++++++++++++++++++++++-------- src/commands/keyset/tsig.rs | 82 +++++++++++++++++++++++++++++++------ 2 files changed, 122 insertions(+), 29 deletions(-) diff --git a/src/commands/keyset/cmd.rs b/src/commands/keyset/cmd.rs index d3a10086..9a7754aa 100644 --- a/src/commands/keyset/cmd.rs +++ b/src/commands/keyset/cmd.rs @@ -2,7 +2,7 @@ #![warn(missing_docs)] #![warn(clippy::missing_docs_in_private_items)] -use crate::commands::keyset::tsig::TsigKeyStore; +use crate::commands::keyset::tsig::{TsigKeyName, TsigKeyStore}; use crate::env::Env; use crate::error::Error; use crate::util; @@ -310,6 +310,7 @@ enum GetCommands { /// The fields that can be changed with a set command. #[derive(Clone, Debug, Subcommand)] +#[allow(clippy::large_enum_variant)] enum SetCommands { /// Set the use_csk config variable. UseCsk { @@ -1875,7 +1876,7 @@ pub struct NameserverConnectionDetails { pub addr: SocketAddr, /// Optional TSIG key to use when communicating with this nameserver. - pub tsig_key_name: Option, + pub tsig_key_name: Option, } impl From<&IpAddr> for NameserverConnectionDetails { @@ -1901,7 +1902,15 @@ impl TryFrom<&str> for NameserverConnectionDetails { let addr = addr_port .parse() .map_err(|e| format!("unable to parse address {addr_port}: {e}"))?; - let tsig_key_name = iter.next().map(|v| v.to_string()); + + let tsig_key_name = match iter.next() { + Some(name) => Some( + Name::from_str(name) + .map_err(|err| format!("Invalid TSIG key name '{name}': {err}"))?, + ), + None => None, + }; + Ok(Self { addr, tsig_key_name, @@ -2253,14 +2262,41 @@ impl WorkSpace { self.config.tsig_store_path = opt_path; } SetCommands::PublicationNameservers { addrs } => { - self.config.nameservers = HashSet::new(); + let mut nameservers = HashSet::new(); + for a in addrs { // When adding nameservers, check that referenced TSIG // keys are in the TSIG store. - self.config - .nameservers - .insert(NameserverConnectionDetails::try_from(a.as_str())?); + nameservers.insert(NameserverConnectionDetails::try_from(a.as_str())?); } + + if nameservers.iter().any(|ns| ns.tsig_key_name.is_some()) { + let Some(key_store_path) = &self.config.tsig_store_path else { + return Err("keyset set tsig-store-path MUST be called first".into()); + }; + + let key_store_file = file_with_write_lock(key_store_path)?; + let key_store: TsigKeyStore = serde_json::from_reader(&key_store_file) + .map_err(|e| { + format!("error loading {}: {e}\n", key_store_path.display()) + })?; + + for tsig_key_name in nameservers + .iter() + .filter_map(|ns| ns.tsig_key_name.as_ref()) + { + // Verify that the key exists in the key store. + if key_store.get(tsig_key_name).is_none() { + return Err(format!( + "No TSIG key with name '{tsig_key_name}' found in store '{}'", + key_store_path.display() + ) + .into()); + } + } + } + + self.config.nameservers = nameservers; } } self.config_changed = true; @@ -5274,18 +5310,17 @@ async fn check_zone( }; // Prepare the named TSIG key for use, if any. - let tsig_key = ns.tsig_key_name.as_ref().and_then(|name| { - match Name::from_str(name) { - Ok(name) => { - tsig_store.map.get(&name).and_then(|key| { - domain::tsig::Key::new(key.alg.into(), &key.data, name, None, None) - .inspect_err(|_err| { /* TODO: Log error */ }) - .ok() - }) + let tsig_key = if let Some(name) = ns.tsig_key_name.as_ref() { + match tsig_store.get(name) { + Some(key) => Some(key), + None => { + warn!("Unknown TSIG key name '{name}'"); + continue; } - Err(_err) => None, // TODO: Log error } - }); + } else { + None + }; // If we have a TSIG key, setup a TSIG capable transport, otherwise // use a normal transport. Use Multi types because only those support diff --git a/src/commands/keyset/tsig.rs b/src/commands/keyset/tsig.rs index 0907c76c..4a9cd7af 100644 --- a/src/commands/keyset/tsig.rs +++ b/src/commands/keyset/tsig.rs @@ -1,24 +1,55 @@ +//! RFC 8945 TSIG support for dnst keyset. +//! +//! This module enables dnst keyset to load TSIG key metadata and secret +//! material from a persisted key store. +//! +//! At the time of writing dnst keyset itself is only able to read from a key +//! store that was persisted by the initial beta version of [Cascade] and as +//! such the persistence format is compatible with and based on that of +//! [Cascade]. +//! +//! Support for actually signing with TSIG keys is provided by the [domain] +//! crate and so this module also provides conversions from our types to those +//! of the domain crate. +//! +//! [RFC 8945]: https://www.rfc-editor.org/rfc/rfc8945.html +//! [Cascade]: https://nlnetlabs.nl/cascade +//! [domain]: https://nlnetlabs.nl/domain use std::collections::HashMap; use domain::base::Name; use serde::{Deserialize, Serialize}; +//------------ AlgSpec ------------------------------------------------------- + +/// A Cascade (de)serialization compatible TSIG algorithm specification. +/// +/// A subset of the [IANA TSIG algorithm name registry]. +/// +/// [IANA TSIG algorithm name registry]: https://www.iana.org/assignments/tsig-algorithm-names/tsig-algorithm-names.xhtml #[derive(Copy, Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] pub enum AlgSpec { - /// SHA-1. + /// hmac-sha1. + #[serde(rename = "hmac-sha1")] HmacSha1, - /// SHA-256. + /// hmac-sha256. + #[serde(rename = "hmac-sha256")] HmacSha256, - /// SHA-384, + /// hmac-sha384, + #[serde(rename = "hmac-sha384")] HmacSha384, - /// SHA-512. + /// hmac-sha512. + #[serde(rename = "hmac-sha512")] HmacSha512, } +//--- impl From + +/// Support conversion from domain TSIG algorithm identifiers to our +/// equivalent. impl From for domain::tsig::Algorithm { fn from(alg: AlgSpec) -> Self { match alg { @@ -30,7 +61,9 @@ impl From for domain::tsig::Algorithm { } } -/// A TSIG key. +//------------ KeySpec ------------------------------------------------------ + +/// A Casdade (de)serialization compatible TSIG key specification. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct KeySpec { @@ -42,10 +75,12 @@ pub struct KeySpec { pub data: Box<[u8]>, } +/// Support for deserializing from base64 to Box<[u8]i> and vice versa. mod tsig_base64 { use domain::utils::base64; use serde::{Deserialize, Deserializer, Serialize, Serializer}; + /// Serialize from a byte slice to a base64 encoded string. pub fn serialize(data: &[u8], serializer: S) -> Result where S: Serializer, @@ -53,6 +88,7 @@ mod tsig_base64 { base64::encode_string(data).serialize(serializer) } + /// Deserialize from a base64 encoded string to a boxed byte array. pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, @@ -63,37 +99,59 @@ mod tsig_base64 { } } +//------------ TsigKeyName --------------------------------------------------- + +/// A Cascade (de)serialization compatible TSIG key name. pub type TsigKeyName = Name>; +//------------ TsigKeyStoreVersion ------------------------------------------- + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub enum TsigKeyStoreVersion { + V1, +} + +//------------ TsigKeyStore -------------------------------------------------- + +/// A Cascade (de)serialization compatible TSIG key store. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct TsigKeyStore { /// The data format version of the store file. - pub version: String, + pub version: TsigKeyStoreVersion, - /// A mapping of names to TSIG key details. + /// A mapping of TSIG key names to key details. pub map: HashMap, } impl TsigKeyStore { + /// Create an empty TSIG key store. pub fn new() -> Self { Self { - version: "v1".to_string(), + version: TsigKeyStoreVersion::V1, map: HashMap::new(), } } - pub fn get(&self, name: &TsigKeyName) -> Result, String> { + /// Get the TSIG key corresponding to the given key name, if any. + /// + /// Returns Some(key) if the key was found, None otherwise. + pub fn get(&self, name: &TsigKeyName) -> Option { if let Some(key) = self.map.get(name) { domain::tsig::Key::new(key.alg.into(), &key.data, name.to_owned(), None, None) .map(Option::Some) - .map_err(|err| err.to_string()) + .unwrap_or_else(|_err| { + unreachable!("domain::tsig::Key::new() can only fail with non-None arguments") + }) } else { - Ok(None) + None } } } +//--- impl Default + impl Default for TsigKeyStore { fn default() -> Self { Self::new() From 7d50ee9680bd4445d06c86fe17c061c4ab9dad76 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Thu, 9 Apr 2026 10:15:27 +0200 Subject: [PATCH 10/15] Switch back to caret. --- src/commands/keyset/cmd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/keyset/cmd.rs b/src/commands/keyset/cmd.rs index 9a7754aa..e72702aa 100644 --- a/src/commands/keyset/cmd.rs +++ b/src/commands/keyset/cmd.rs @@ -1895,7 +1895,7 @@ impl TryFrom<&str> for NameserverConnectionDetails { // a port is required, there is no default port. TODO: allow hostnames // and allow the port to be optional. fn try_from(s: &str) -> Result { - let mut iter = s.split('!'); + let mut iter = s.split('^'); let Some(addr_port) = iter.next() else { return Err("Address expected".into()); }; From cadaf0b7e31dbc527f749a06efe7160cbd77b5a6 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:36:42 +0200 Subject: [PATCH 11/15] Document the new TSIG related set arguments in the man page. --- doc/manual/build/man/dnst-key2ds.1 | 2 +- doc/manual/build/man/dnst-keygen.1 | 2 +- doc/manual/build/man/dnst-keyset.1 | 33 +++++++++++++++++++-- doc/manual/build/man/dnst-notify.1 | 2 +- doc/manual/build/man/dnst-nsec3-hash.1 | 2 +- doc/manual/build/man/dnst-signzone.1 | 2 +- doc/manual/build/man/dnst-update.1 | 2 +- doc/manual/build/man/dnst.1 | 2 +- doc/manual/build/man/ldns-key2ds.1 | 2 +- doc/manual/build/man/ldns-keygen.1 | 2 +- doc/manual/build/man/ldns-notify.1 | 2 +- doc/manual/build/man/ldns-nsec3-hash.1 | 2 +- doc/manual/build/man/ldns-signzone.1 | 2 +- doc/manual/build/man/ldns-update.1 | 2 +- doc/manual/source/man/dnst-keyset.rst | 40 ++++++++++++++++++++++++-- 15 files changed, 81 insertions(+), 18 deletions(-) diff --git a/doc/manual/build/man/dnst-key2ds.1 b/doc/manual/build/man/dnst-key2ds.1 index 52c3f8cf..cee7acd7 100644 --- a/doc/manual/build/man/dnst-key2ds.1 +++ b/doc/manual/build/man/dnst-key2ds.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-KEY2DS" "1" "Mar 05, 2026" "0.2.0-alpha1" "dnst" +.TH "DNST-KEY2DS" "1" "Apr 09, 2026" "0.2.0-alpha1" "dnst" .SH NAME dnst-key2ds \- Generate DS RRs from the DNSKEYs in a keyfile .SH SYNOPSIS diff --git a/doc/manual/build/man/dnst-keygen.1 b/doc/manual/build/man/dnst-keygen.1 index 4edbfdb2..53b8ac08 100644 --- a/doc/manual/build/man/dnst-keygen.1 +++ b/doc/manual/build/man/dnst-keygen.1 @@ -28,7 +28,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-KEYGEN" "1" "Mar 05, 2026" "0.2.0-alpha1" "dnst" +.TH "DNST-KEYGEN" "1" "Apr 09, 2026" "0.2.0-alpha1" "dnst" .SH NAME dnst-keygen \- Generate a new key pair for a domain name .SH SYNOPSIS diff --git a/doc/manual/build/man/dnst-keyset.1 b/doc/manual/build/man/dnst-keyset.1 index 56cf827e..e31a5552 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" "Mar 05, 2026" "0.2.0-alpha1" "dnst" +.TH "DNST-KEYSET" "1" "Apr 09, 2026" "0.2.0-alpha1" "dnst" .SH NAME dnst-keyset \- Manage DNSSEC signing keys for a domain .SH SYNOPSIS @@ -216,8 +216,9 @@ For the \fBReportDnskeyPropagated\fP and \fBReportDsPropagated\fP actions, each the queried to see if the DNSKEY RRset or DS RRset match the KSKs. The \fBReportRrsigPropagated\fP action is more complex. -First the entire zone is transferred from the primary nameserver listed in the -SOA record. +First the entire zone is transferred from the nameservers specified via +\fBset publication\-nameservers\fP, or if not set form the primary nameserver +listed in the SOA record. Then all relevant signatures are checked if they have the expected key tags. The maximum TTL in the zone is recorded to be reported. Finally, all addresses of listed nameservers are checked to see if they @@ -806,6 +807,32 @@ 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 +tsig\-store\-path +.sp +Set the path to a TSIG key store file to use. +.INDENT 2.0 +.TP +.B Keys defined in the store file must use one of the following algorithms: +hmac\-sha1, hmac\-sha256, hmac\-sha384 or hmac\-sha512 +.UNINDENT +.sp +Note: Currently there is no way to create this file using \fBdnst +keyset\fP\&. The file is in JSON format and defines zero or more TSIG keys +as entries in a map. The example below defines a single TSIG key with name +\fBtsig\-zonedata\-ch\-public\-21\-03\fP using algorithm \fBhmac\-sha512\fP with a +base64 encoded secret. +.IP \(bu 2 +publication\-nameservers +.sp +Set the nameservers to transfer from when checking a zone. +.sp +If no nameserver values are specified the default behaviour of querying +the primary nameserver defined in the SOA record will be used. +.sp +Nameservers should be specified as space separated +arguments, each nameserver being one argument in the form +\fB[:][^[:][^ Date: Thu, 9 Apr 2026 15:19:45 +0200 Subject: [PATCH 12/15] Fix missing code-block in man page. --- doc/manual/build/man/dnst-keyset.1 | 16 ++++++++++++++++ doc/manual/source/man/dnst-keyset.rst | 1 + 2 files changed, 17 insertions(+) diff --git a/doc/manual/build/man/dnst-keyset.1 b/doc/manual/build/man/dnst-keyset.1 index 362ed8ed..176bf443 100644 --- a/doc/manual/build/man/dnst-keyset.1 +++ b/doc/manual/build/man/dnst-keyset.1 @@ -819,6 +819,22 @@ keyset\fP\&. The file is in JSON format and defines zero or more TSIG keys as entries in a map. The example below defines a single TSIG key with name \fBtsig\-zonedata\-ch\-public\-21\-03\fP using algorithm \fBhmac\-sha512\fP with a base64 encoded secret. +.INDENT 2.0 +.INDENT 3.5 +.sp +.EX +{ + \(dqversion\(dq: \(dqv1\(dq, + \(dqmap\(dq: { + \(dqtsig\-zonedata\-ch\-public\-21\-01\(dq: { + \(dqalg\(dq: \(dqhmac\-sha512\(dq, + \(dqdata\(dq: \(dqstZw...iJ3Q==\(dq + } + } +} +.EE +.UNINDENT +.UNINDENT .IP \(bu 2 publication\-nameservers .sp diff --git a/doc/manual/source/man/dnst-keyset.rst b/doc/manual/source/man/dnst-keyset.rst index 13294ce9..ac4827b8 100644 --- a/doc/manual/source/man/dnst-keyset.rst +++ b/doc/manual/source/man/dnst-keyset.rst @@ -799,6 +799,7 @@ The keyset subcommand provides the following commands: base64 encoded secret. .. code-block:: json + { "version": "v1", "map": { From 47142ab48a2f7d7b270e106874e0f87366e27f1f Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:20:23 +0200 Subject: [PATCH 13/15] Make port number for set publication-servers mandatory. --- doc/manual/build/man/dnst-keyset.1 | 2 +- doc/manual/source/man/dnst-keyset.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/manual/build/man/dnst-keyset.1 b/doc/manual/build/man/dnst-keyset.1 index 176bf443..7040c2df 100644 --- a/doc/manual/build/man/dnst-keyset.1 +++ b/doc/manual/build/man/dnst-keyset.1 @@ -845,7 +845,7 @@ the primary nameserver defined in the SOA record will be used. .sp Nameservers should be specified as space separated arguments, each nameserver being one argument in the form -\fB[:][^:[^[:][^:[^ Date: Thu, 9 Apr 2026 15:25:14 +0200 Subject: [PATCH 14/15] Minor tweaks. --- doc/manual/build/man/dnst-keyset.1 | 6 +++--- doc/manual/source/man/dnst-keyset.rst | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/manual/build/man/dnst-keyset.1 b/doc/manual/build/man/dnst-keyset.1 index 7040c2df..3b07fa0a 100644 --- a/doc/manual/build/man/dnst-keyset.1 +++ b/doc/manual/build/man/dnst-keyset.1 @@ -814,9 +814,9 @@ Set the path to a TSIG key store file to use. hmac\-sha1, hmac\-sha256, hmac\-sha384 or hmac\-sha512 .UNINDENT .sp -Note: Currently there is no way to create this file using \fBdnst -keyset\fP\&. The file is in JSON format and defines zero or more TSIG keys -as entries in a map. The example below defines a single TSIG key with name +Currently there is no way to create this file using \fBdnst keyset\fP\&. +The file is in JSON format and defines zero or more TSIG keys as +entries in a map. The example below defines a single TSIG key with name \fBtsig\-zonedata\-ch\-public\-21\-03\fP using algorithm \fBhmac\-sha512\fP with a base64 encoded secret. .INDENT 2.0 diff --git a/doc/manual/source/man/dnst-keyset.rst b/doc/manual/source/man/dnst-keyset.rst index adfc8963..79a97485 100644 --- a/doc/manual/source/man/dnst-keyset.rst +++ b/doc/manual/source/man/dnst-keyset.rst @@ -789,12 +789,12 @@ The keyset subcommand provides the following commands: Set the path to a TSIG key store file to use. - Keys defined in the store file must use one of the following algorithms: + Keys defined in the store file must use one of the following algorithms\: hmac-sha1, hmac-sha256, hmac-sha384 or hmac-sha512 - Note: Currently there is no way to create this file using ``dnst - keyset``. The file is in JSON format and defines zero or more TSIG keys - as entries in a map. The example below defines a single TSIG key with name + Currently there is no way to create this file using ``dnst keyset``. + The file is in JSON format and defines zero or more TSIG keys as + entries in a map. The example below defines a single TSIG key with name ``tsig-zonedata-ch-public-21-03`` using algorithm ``hmac-sha512`` with a base64 encoded secret. From c83a19f5ea38c6612ed3ba6d9325ffe82da87579 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:29:10 +0200 Subject: [PATCH 15/15] Minor tweaks. --- doc/manual/build/man/dnst-keyset.1 | 25 ++++++++++++++++++++----- doc/manual/source/man/dnst-keyset.rst | 13 +++++++++---- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/doc/manual/build/man/dnst-keyset.1 b/doc/manual/build/man/dnst-keyset.1 index 3b07fa0a..179f13c2 100644 --- a/doc/manual/build/man/dnst-keyset.1 +++ b/doc/manual/build/man/dnst-keyset.1 @@ -808,10 +808,21 @@ by the parent zone to update the DS records automatically. tsig\-store\-path .sp Set the path to a TSIG key store file to use. +.sp +Keys defined in the store file must use one of the following algorithms: .INDENT 2.0 -.TP -.B Keys defined in the store file must use one of the following algorithms: -hmac\-sha1, hmac\-sha256, hmac\-sha384 or hmac\-sha512 +.INDENT 3.5 +.INDENT 0.0 +.IP \(bu 2 +hmac\-sha1 +.IP \(bu 2 +hmac\-sha256 +.IP \(bu 2 +hmac\-sha384 +.IP \(bu 2 +hmac\-sha512 +.UNINDENT +.UNINDENT .UNINDENT .sp Currently there is no way to create this file using \fBdnst keyset\fP\&. @@ -844,8 +855,12 @@ If no nameserver values are specified the default behaviour of querying the primary nameserver defined in the SOA record will be used. .sp Nameservers should be specified as space separated -arguments, each nameserver being one argument in the form -\fB:[^:[^] +.UNINDENT +.UNINDENT .IP \(bu 2 fake\-time .sp diff --git a/doc/manual/source/man/dnst-keyset.rst b/doc/manual/source/man/dnst-keyset.rst index 79a97485..aaa17745 100644 --- a/doc/manual/source/man/dnst-keyset.rst +++ b/doc/manual/source/man/dnst-keyset.rst @@ -789,8 +789,12 @@ The keyset subcommand provides the following commands: Set the path to a TSIG key store file to use. - Keys defined in the store file must use one of the following algorithms\: - hmac-sha1, hmac-sha256, hmac-sha384 or hmac-sha512 + Keys defined in the store file must use one of the following algorithms: + + - hmac-sha1 + - hmac-sha256 + - hmac-sha384 + - hmac-sha512 Currently there is no way to create this file using ``dnst keyset``. The file is in JSON format and defines zero or more TSIG keys as @@ -818,8 +822,9 @@ The keyset subcommand provides the following commands: the primary nameserver defined in the SOA record will be used. Nameservers should be specified as space separated - arguments, each nameserver being one argument in the form - ``:[^:[^] * fake-time