From 48d26d8ce15088fe4a8e5e7c0f9e234f8ae320c9 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:41:10 +0100 Subject: [PATCH 1/5] Merge PR #444 branch zonemd-from-str into this branch. --- src/base/iana/mod.rs | 2 + src/base/iana/zonemd.rs | 50 +++++++++++++++++ src/rdata/zonemd.rs | 118 ++++------------------------------------ 3 files changed, 62 insertions(+), 108 deletions(-) create mode 100644 src/base/iana/zonemd.rs diff --git a/src/base/iana/mod.rs b/src/base/iana/mod.rs index 2b73fe62..9d86b2e9 100644 --- a/src/base/iana/mod.rs +++ b/src/base/iana/mod.rs @@ -35,6 +35,7 @@ pub use self::rcode::{OptRcode, Rcode, TsigRcode}; pub use self::rtype::Rtype; pub use self::secalg::SecAlg; pub use self::svcb::SvcParamKey; +pub use self::zonemd::{ZonemdAlg, ZonemdScheme}; #[macro_use] mod macros; @@ -49,3 +50,4 @@ pub mod rcode; pub mod rtype; pub mod secalg; pub mod svcb; +pub mod zonemd; diff --git a/src/base/iana/zonemd.rs b/src/base/iana/zonemd.rs new file mode 100644 index 00000000..24944847 --- /dev/null +++ b/src/base/iana/zonemd.rs @@ -0,0 +1,50 @@ +//! ZONEMD IANA parameters. + +//------------ ZonemdScheme -------------------------------------------------- + +int_enum! { + /// ZONEMD schemes. + /// + /// This type selects the method by which data is collated and presented + /// as input to the hashing function for use with [ZONEMD]. + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2024-11-29. + /// + /// [ZONEMD]: ../../../rdata/zonemd/index.html + /// [IANA registration]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#zonemd-schemes + => + ZonemdScheme, u8; + + /// Specifies that the SIMPLE scheme is used. + (SIMPLE => 1, "SIMPLE") +} + +int_enum_str_decimal!(ZonemdScheme, u8); +int_enum_zonefile_fmt_decimal!(ZonemdScheme, "scheme"); + +//------------ ZonemdAlg ----------------------------------------------------- + +int_enum! { + /// ZONEMD algorithms. + /// + /// This type selects the algorithm used to hash domain names for use with + /// the [ZONEMD]. + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2024-11-29. + /// + /// [ZONEMD]: ../../../rdata/zonemd/index.html + /// [IANA registration]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#zonemd-hash-algorithms + => + ZonemdAlg, u8; + + /// Specifies that the SHA-384 algorithm is used. + (SHA384 => 1, "SHA-384") + + /// Specifies that the SHA-512 algorithm is used. + (SHA512 => 2, "SHA-512") +} + +int_enum_str_decimal!(ZonemdAlg, u8); +int_enum_zonefile_fmt_decimal!(ZonemdAlg, "hash algorithm"); diff --git a/src/rdata/zonemd.rs b/src/rdata/zonemd.rs index 66f41d40..025dae20 100644 --- a/src/rdata/zonemd.rs +++ b/src/rdata/zonemd.rs @@ -9,7 +9,7 @@ #![allow(clippy::needless_maybe_sized)] use crate::base::cmp::CanonicalOrd; -use crate::base::iana::Rtype; +use crate::base::iana::{Rtype, ZonemdAlg, ZonemdScheme}; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::scan::{Scan, Scanner}; use crate::base::serial::Serial; @@ -29,8 +29,8 @@ const DIGEST_MIN_LEN: usize = 12; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Zonemd { serial: Serial, - scheme: Scheme, - algo: Algorithm, + scheme: ZonemdScheme, + algo: ZonemdAlg, #[cfg_attr( feature = "serde", serde( @@ -54,8 +54,8 @@ impl Zonemd { /// Create a Zonemd record data from provided parameters. pub fn new( serial: Serial, - scheme: Scheme, - algo: Algorithm, + scheme: ZonemdScheme, + algo: ZonemdAlg, digest: Octs, ) -> Self { Self { @@ -72,12 +72,12 @@ impl Zonemd { } /// Get the scheme field. - pub fn scheme(&self) -> Scheme { + pub fn scheme(&self) -> ZonemdScheme { self.scheme } /// Get the hash algorithm field. - pub fn algorithm(&self) -> Algorithm { + pub fn algorithm(&self) -> ZonemdAlg { self.algo } @@ -233,20 +233,7 @@ impl> ZonefileFmt for Zonemd { p.block(|p| { p.write_token(self.serial)?; p.write_show(self.scheme)?; - p.write_comment(format_args!("scheme ({})", match self.scheme { - Scheme::Reserved => "reserved", - Scheme::Simple => "simple", - Scheme::Unassigned(_) => "unassigned", - Scheme::Private(_) => "private", - }))?; p.write_show(self.algo)?; - p.write_comment(format_args!("algorithm ({})", match self.algo { - Algorithm::Reserved => "reserved", - Algorithm::Sha384 => "SHA384", - Algorithm::Sha512 => "SHA512", - Algorithm::Unassigned(_) => "unassigned", - Algorithm::Private(_) => "private", - }))?; p.write_token(base16::encode_display(&self.digest)) }) } @@ -314,92 +301,6 @@ impl> Ord for Zonemd { } } -/// The data collation scheme. -/// -/// This enumeration wraps an 8-bit unsigned integer that identifies the -/// methods by which data is collated and presented as input to the -/// hashing function. -#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Scheme { - Reserved, - Simple, - Unassigned(u8), - Private(u8), -} - -impl From for u8 { - fn from(s: Scheme) -> u8 { - match s { - Scheme::Reserved => 0, - Scheme::Simple => 1, - Scheme::Unassigned(n) => n, - Scheme::Private(n) => n, - } - } -} - -impl From for Scheme { - fn from(n: u8) -> Self { - match n { - 0 | 255 => Self::Reserved, - 1 => Self::Simple, - 2..=239 => Self::Unassigned(n), - 240..=254 => Self::Private(n), - } - } -} - -impl ZonefileFmt for Scheme { - fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { - p.write_token(u8::from(*self)) - } -} - -/// The Hash Algorithm used to construct the digest. -/// -/// This enumeration wraps an 8-bit unsigned integer that identifies -/// the cryptographic hash algorithm. -#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Algorithm { - Reserved, - Sha384, - Sha512, - Unassigned(u8), - Private(u8), -} - -impl From for u8 { - fn from(algo: Algorithm) -> u8 { - match algo { - Algorithm::Reserved => 0, - Algorithm::Sha384 => 1, - Algorithm::Sha512 => 2, - Algorithm::Unassigned(n) => n, - Algorithm::Private(n) => n, - } - } -} - -impl From for Algorithm { - fn from(n: u8) -> Self { - match n { - 0 | 255 => Self::Reserved, - 1 => Self::Sha384, - 2 => Self::Sha512, - 3..=239 => Self::Unassigned(n), - 240..=254 => Self::Private(n), - } - } -} - -impl ZonefileFmt for Algorithm { - fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { - p.write_token(u8::from(*self)) - } -} - #[cfg(test)] #[cfg(all(feature = "std", feature = "bytes"))] mod test { @@ -437,6 +338,7 @@ mod test { #[cfg(feature = "zonefile")] #[test] fn zonemd_parse_zonefile() { + use crate::base::iana::ZonemdAlg; use crate::base::Name; use crate::rdata::ZoneRecordData; use crate::zonefile::inplace::{Entry, Zonefile}; @@ -469,8 +371,8 @@ ns2 3600 IN AAAA 2001:db8::63 match record.into_data() { ZoneRecordData::Zonemd(rd) => { assert_eq!(2018031900, rd.serial().into_int()); - assert_eq!(Scheme::Simple, rd.scheme()); - assert_eq!(Algorithm::Sha384, rd.algorithm()); + assert_eq!(ZonemdScheme::SIMPLE, rd.scheme()); + assert_eq!(ZonemdAlg::SHA384, rd.algorithm()); } _ => panic!(), } From 5ede42e24c3cf5e1618d40e1b70334c9decc9030 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:24:12 +0100 Subject: [PATCH 2/5] IANA ZONEMD algorithm mnemonics are not hyphenated. --- src/base/iana/zonemd.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/base/iana/zonemd.rs b/src/base/iana/zonemd.rs index 24944847..cd92a101 100644 --- a/src/base/iana/zonemd.rs +++ b/src/base/iana/zonemd.rs @@ -40,10 +40,10 @@ int_enum! { ZonemdAlg, u8; /// Specifies that the SHA-384 algorithm is used. - (SHA384 => 1, "SHA-384") + (SHA384 => 1, "SHA384") /// Specifies that the SHA-512 algorithm is used. - (SHA512 => 2, "SHA-512") + (SHA512 => 2, "SHA512") } int_enum_str_decimal!(ZonemdAlg, u8); From 85ffaf745306fdd405a36f619ecb5593bc6b1bb6 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:22:03 +0100 Subject: [PATCH 3/5] Add a tab before the RDATA as well as within it, to match LDNS tabbed output format. (#463) --- src/base/zonefile_fmt.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/base/zonefile_fmt.rs b/src/base/zonefile_fmt.rs index b48e48e1..8902d7cf 100644 --- a/src/base/zonefile_fmt.rs +++ b/src/base/zonefile_fmt.rs @@ -145,6 +145,7 @@ impl FormatWriter for SimpleWriter { /// A single line writer that puts tabs between ungrouped tokens struct TabbedWriter { first: bool, + first_block: bool, blocks: usize, writer: W, } @@ -153,6 +154,7 @@ impl TabbedWriter { fn new(writer: W) -> Self { Self { first: true, + first_block: true, blocks: 0, writer, } @@ -162,7 +164,14 @@ impl TabbedWriter { impl FormatWriter for TabbedWriter { fn fmt_token(&mut self, args: fmt::Arguments<'_>) -> Result { if !self.first { - let c = if self.blocks == 0 { '\t' } else { ' ' }; + let c = if self.blocks == 0 { + '\t' + } else if self.first_block { + self.first_block = false; + '\t' + } else { + ' ' + }; self.writer.write_char(c)?; } self.first = false; @@ -439,7 +448,7 @@ mod test { } #[test] - fn aligned() { + fn tabbed() { let record = create_record( Cds::new( 5414, @@ -453,7 +462,7 @@ mod test { // The name, ttl, class and rtype should be separated by \t, but the // rdata shouldn't. assert_eq!( - "example.com.\t3600\tIN\tCDS 5414 15 2 DEADBEEF", + "example.com.\t3600\tIN\tCDS\t5414 15 2 DEADBEEF", record.display_zonefile(DisplayKind::Tabbed).to_string() ); } From 254dc9c958206422b70380469dfb362552ef1aff Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:27:19 +0100 Subject: [PATCH 4/5] Update changelog. --- Changelog.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 0845dde6..aabf4b9a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -17,7 +17,8 @@ New running resolver. In combination with `ResolvConf::new()` this can also be used to control the connections made when testing code that uses the stub resolver. ([#440]) -* Add `ZonefileFmt` trait for printing records as zonefiles. ([#379], [#446]) +* Add `ZonefileFmt` trait for printing records as zonefiles. ([#379], [#446], + [#463]) Bug fixes @@ -48,6 +49,7 @@ Other changes [#440]: https://github.com/NLnetLabs/domain/pull/440 [#441]: https://github.com/NLnetLabs/domain/pull/441 [#446]: https://github.com/NLnetLabs/domain/pull/446 +[#463]: https://github.com/NLnetLabs/domain/pull/463 [@weilence]: https://github.com/weilence ## 0.10.3 From f00acc6967414c31064600a2b867fb1d5761030a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:20:29 +0100 Subject: [PATCH 5/5] WIP: Use a hash provider. --- src/sign/records.rs | 213 +++++++++++++++++++++++++++++--------------- src/validate.rs | 6 ++ 2 files changed, 146 insertions(+), 73 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 3565f5c6..354623cb 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -7,7 +7,7 @@ use core::ops::Deref; use core::slice::Iter; use std::boxed::Box; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::fmt::Debug; use std::hash::Hash; use std::string::{String, ToString}; @@ -385,14 +385,16 @@ where /// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155.html /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077.html /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html - pub fn nsec3s( + // TODO: Move to Signer and do HashProvider = OnDemandNsec3HashProvider + // TODO: Does it make sense to take both Nsec3param AND HashProvider as input? + pub fn nsec3s( &self, apex: &FamilyName, ttl: Ttl, params: Nsec3param, opt_out: Nsec3OptOut, assume_dnskeys_will_be_added: bool, - capture_hash_to_owner_mappings: bool, + hash_provider: &mut HashProvider, ) -> Result, Nsec3HashError> where N: ToName + Clone + From> + Display + Ord + Hash, @@ -407,6 +409,7 @@ where + EmptyBuilder + FreezeBuilder, ::Octets: AsRef<[u8]>, + HashProvider: Nsec3HashProvider, { // TODO: // - Handle name collisions? (see RFC 5155 7.1 Zone Signing) @@ -445,11 +448,11 @@ where let apex_label_count = apex_owner.iter_labels().count(); let mut last_nent_stack: Vec = vec![]; - let mut nsec3_hash_map = if capture_hash_to_owner_mappings { - Some(HashMap::::new()) - } else { - None - }; + // let mut nsec3_hash_map = if capture_hash_to_owner_mappings { + // Some(HashMap::::new()) + // } else { + // None + // }; for family in families { // If the owner is out of zone, we have moved out of our zone and @@ -567,6 +570,7 @@ where let rec: Record> = Self::mk_nsec3( name.owner(), + hash_provider, params.hash_algorithm(), nsec3_flags, params.iterations(), @@ -576,10 +580,10 @@ where ttl, )?; - if let Some(nsec3_hash_map) = &mut nsec3_hash_map { - nsec3_hash_map - .insert(rec.owner().clone(), name.owner().clone()); - } + // if let Some(nsec3_hash_map) = &mut nsec3_hash_map { + // nsec3_hash_map + // .insert(rec.owner().clone(), name.owner().clone()); + // } // Store the record by order of its owner name. nsec3s.push(rec); @@ -596,6 +600,7 @@ where let rec = Self::mk_nsec3( &name, + hash_provider, params.hash_algorithm(), nsec3_flags, params.iterations(), @@ -605,9 +610,9 @@ where ttl, )?; - if let Some(nsec3_hash_map) = &mut nsec3_hash_map { - nsec3_hash_map.insert(rec.owner().clone(), name); - } + // if let Some(nsec3_hash_map) = &mut nsec3_hash_map { + // nsec3_hash_map.insert(rec.owner().clone(), name); + // } // Store the record by order of its owner name. nsec3s.push(rec); @@ -656,11 +661,11 @@ where let res = Nsec3Records::new(nsec3s.records, nsec3param); - if let Some(nsec3_hash_map) = nsec3_hash_map { - Ok(res.with_hashes(nsec3_hash_map)) - } else { - Ok(res) - } + // if let Some(nsec3_hash_map) = nsec3_hash_map { + // Ok(res.with_hashes(nsec3_hash_map)) + // } else { + Ok(res) + // } } pub fn write(&self, target: &mut W) -> Result<(), fmt::Error> @@ -719,13 +724,14 @@ where S: Sorter, { #[allow(clippy::too_many_arguments)] - fn mk_nsec3( + fn mk_nsec3( name: &N, + hash_provider: &mut HashProvider, alg: Nsec3HashAlg, flags: u8, iterations: u16, salt: &Nsec3Salt, - apex_owner: &N, + _apex_owner: &N, bitmap: RtypeBitmapBuilder<::Builder>, ttl: Ttl, ) -> Result>, Nsec3HashError> @@ -735,14 +741,12 @@ where ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]> + Truncate, Nsec3: Into, + HashProvider: Nsec3HashProvider, { - // Create the base32hex ENT NSEC owner name. - let base32hex_label = - Self::mk_base32hex_label_for_name(name, alg, iterations, salt)?; - - // Prepend it to the zone name to create the NSEC3 owner - // name. - let owner_name = Self::append_origin(base32hex_label, apex_owner); + // let owner_name = mk_hashed_nsec3_owner_name( + // name, alg, iterations, salt, apex_owner, + // )?; + let owner_name = hash_provider.get_or_create(name)?; // RFC 5155 7.1. step 2: // "The Next Hashed Owner Name field is left blank for the moment." @@ -762,35 +766,6 @@ where Ok(Record::new(owner_name, Class::IN, ttl, nsec3)) } - - fn append_origin(base32hex_label: String, apex_owner: &N) -> N - where - N: ToName + From>, - Octets: FromBuilder, - ::Builder: - EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - { - let mut builder = NameBuilder::::new(); - builder.append_label(base32hex_label.as_bytes()).unwrap(); - let owner_name = builder.append_origin(apex_owner).unwrap(); - let owner_name: N = owner_name.into(); - owner_name - } - - fn mk_base32hex_label_for_name( - name: &N, - alg: Nsec3HashAlg, - iterations: u16, - salt: &Nsec3Salt, - ) -> Result - where - N: ToName, - Octets: AsRef<[u8]>, - { - let hash_octets: Vec = - nsec3_hash(name, alg, iterations, salt)?.into_octets(); - Ok(base32::encode_string_hex(&hash_octets).to_ascii_lowercase()) - } } impl Default @@ -855,11 +830,6 @@ pub struct Nsec3Records { /// The NSEC3PARAM record. pub param: Record>, - - /// A map of hashes to owner names. - /// - /// For diagnostic purposes. None if not generated. - pub hashes: Option>, } impl Nsec3Records { @@ -867,16 +837,7 @@ impl Nsec3Records { recs: Vec>>, param: Record>, ) -> Self { - Self { - recs, - param, - hashes: None, - } - } - - pub fn with_hashes(mut self, hashes: HashMap) -> Self { - self.hashes = Some(hashes); - self + Self { recs, param } } } @@ -1819,3 +1780,109 @@ where )) } } + +pub fn mk_hashed_nsec3_owner_name( + name: &N, + alg: Nsec3HashAlg, + iterations: u16, + salt: &Nsec3Salt, + apex_owner: &N, +) -> Result +where + N: ToName + From>, + Octs: FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + SaltOcts: AsRef<[u8]>, +{ + let base32hex_label = + mk_base32hex_label_for_name(name, alg, iterations, salt)?; + Ok(append_origin(base32hex_label, apex_owner)) +} + +fn append_origin(base32hex_label: String, apex_owner: &N) -> N +where + N: ToName + From>, + Octs: FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, +{ + let mut builder = NameBuilder::::new(); + builder.append_label(base32hex_label.as_bytes()).unwrap(); + let owner_name = builder.append_origin(apex_owner).unwrap(); + let owner_name: N = owner_name.into(); + owner_name +} + +fn mk_base32hex_label_for_name( + name: &N, + alg: Nsec3HashAlg, + iterations: u16, + salt: &Nsec3Salt, +) -> Result +where + N: ToName, + SaltOcts: AsRef<[u8]>, +{ + let hash_octets: Vec = + nsec3_hash(name, alg, iterations, salt)?.into_octets(); + Ok(base32::encode_string_hex(&hash_octets).to_ascii_lowercase()) +} + +//------------ Nsec3HashProvider --------------------------------------------- + +pub trait Nsec3HashProvider { + fn get_or_create(&mut self, unhashed_owner_name: &N) -> Result; +} + +pub struct OnDemandNsec3HashProvider { + alg: Nsec3HashAlg, + iterations: u16, + salt: Nsec3Salt, + apex_owner: N, +} + +impl OnDemandNsec3HashProvider { + pub fn new( + alg: Nsec3HashAlg, + iterations: u16, + salt: Nsec3Salt, + apex_owner: N, + ) -> Self { + Self { + alg, + iterations, + salt, + apex_owner, + } + } + + pub fn algorithm(&self) -> Nsec3HashAlg { + self.alg + } + + pub fn iterations(&self) -> u16 { + self.iterations + } + + pub fn salt(&self) -> &Nsec3Salt { + &self.salt + } +} + +impl Nsec3HashProvider + for OnDemandNsec3HashProvider +where + N: ToName + From>, + Octs: FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + SaltOcts: AsRef<[u8]>, +{ + fn get_or_create(&mut self, unhashed_owner_name: &N) -> Result { + mk_hashed_nsec3_owner_name( + unhashed_owner_name, + self.alg, + self.iterations, + &self.salt, + &self.apex_owner, + ) + } +} diff --git a/src/validate.rs b/src/validate.rs index b6405ff2..d96c8b13 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1759,6 +1759,9 @@ pub enum Nsec3HashError { /// The hashing process produced a hash that already exists. CollisionDetected, + + /// The hash provider did not provide a hash for the given owner name. + MissingHash, } ///--- Display @@ -1777,6 +1780,9 @@ impl std::fmt::Display for Nsec3HashError { Nsec3HashError::CollisionDetected => { f.write_str("Hash collision detected") } + Nsec3HashError::MissingHash => { + f.write_str("Missing hash for owner name") + } } } }