diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02a0af673..164dfd223 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,7 @@ jobs: run: cargo fmt --all -- --check - run: cargo check --no-default-features --all-targets - run: cargo test --all-features + minimal-versions: name: Check minimal versions runs-on: ubuntu-latest @@ -60,4 +61,53 @@ jobs: run: | cargo +nightly update -Z minimal-versions cargo check --all-features --all-targets --locked - + + # Note: This deliberately doesn't try to run the examples as some of them + # don't terminate and/or attempt to make network connections that will fail + # or perhaps be a nuisance to real name servers if run regularly by CI. + # + # Note: This is just a band aid, ideally there would be a way for Cargo to + # build/run examples using the feature set specified in Cargo.toml itself, + # but that isn't currently possible (see [1]). + # + # [1]: https://github.com/rust-lang/cargo/issues/4663) + build-examples: + name: Build examples + runs-on: ubuntu-latest + strategy: + matrix: + rust: [1.78.0, stable, beta, nightly] + steps: + - name: Checkout repository + uses: actions/checkout@v1 + - name: Install Rust + uses: hecrj/setup-rust-action@v2 + with: + rust-version: ${{ matrix.rust }} + - name: Compile all examples + shell: bash + # TODO: Emit matrix elements based on inspecting Cargo.toml so that + # each example runs as its own GitHub Actions step/job? + run: | + # Generate the set of cargo check --example X --features Y commands to + # run and store them in a temporary script. This command works by + # extracting the example blocks from the Cargo.toml file and the set + # of features that the block indicates are needed to run the example. + # E.g. given this block in Cargo.toml: + # + # [[example]] + # name = "lookup" + # required-features = ["resolv"] + # + # It outputs a line that looks like this: + # + # cargo check --example lookup --features resolv + # + # One line per example is output and the set of lines directed to a temporary + # shell script file which is then run in the next step. + + cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].targets[] | select(.kind[] | contains("example")) | "cargo check --example \(.name) --features \(."required-features" | flatten | join(","))"' > ${{ runner.temp }}/check-examples.sh + + # Run the temporary script: + + bash ${{ runner.temp }}/check-examples.sh diff --git a/Cargo.lock b/Cargo.lock index ca7fb4b69..7f844fa92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,15 @@ dependencies = [ "libc", ] +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arc-swap" version = "1.7.1" @@ -217,10 +226,22 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "domain" version = "0.10.3" dependencies = [ + "arbitrary", "arc-swap", "bytes", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 4c60ad9d7..254b4a5c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ name = "domain" path = "src/lib.rs" [dependencies] +arbitrary = { version = "1.4.1", optional = true, features = ["derive"] } octseq = { version = "0.5.2", default-features = false } time = { version = "0.3.1", default-features = false } rand = { version = "0.8", optional = true } diff --git a/Changelog.md b/Changelog.md index 7a61fdb3c..b31dd490b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,9 +10,12 @@ New rather than a tree. ([#396]) * Changed `fmt::Display` for `HINFO` records to a show a quoted string. ([#421]) +* Added support for `NAPTR` record type. ([#427] by [@weilence]) Bug fixes +* NSEC records should include themselves in the generated bitmap. ([#417]) + Unstable features * `unstable-server-transport` @@ -31,8 +34,10 @@ Other changes [#353]: https://github.com/NLnetLabs/domain/pull/353 [#396]: https://github.com/NLnetLabs/domain/pull/396 -[#421]: https://github.com/NLnetLabs/domain/pull/412 - +[#417]: https://github.com/NLnetLabs/domain/pull/417 +[#421]: https://github.com/NLnetLabs/domain/pull/421 +[#427]: https://github.com/NLnetLabs/domain/pull/427 +[@weilence]: https://github.com/weilence ## 0.10.3 diff --git a/src/base/dig_printer.rs b/src/base/dig_printer.rs new file mode 100644 index 000000000..426b8dc37 --- /dev/null +++ b/src/base/dig_printer.rs @@ -0,0 +1,174 @@ +use core::fmt; + +use crate::rdata::AllRecordData; + +use super::zonefile_fmt::ZonefileFmt; +use super::ParsedRecord; +use super::{opt::AllOptData, Message, Rtype}; + +/// Interal type for printing a message in dig style +/// +/// This is only exposed to users of this library as `impl fmt::Display`. +pub(super) struct DigPrinter<'a, Octs> { + pub msg: &'a Message, +} + +impl<'a, Octs: AsRef<[u8]>> fmt::Display for DigPrinter<'a, Octs> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let msg = self.msg.for_slice_ref(); + + // Header + let header = msg.header(); + let counts = msg.header_counts(); + + writeln!( + f, + ";; ->>HEADER<<- opcode: {}, rcode: {}, id: {}", + header.opcode().display_zonefile(false), + header.rcode(), + header.id() + )?; + write!(f, ";; flags: {}", header.flags())?; + writeln!( + f, + "; QUERY: {}, ANSWER: {}, AUTHORITY: {}, ADDITIONAL: {}", + counts.qdcount(), + counts.ancount(), + counts.nscount(), + counts.arcount() + )?; + + // We need this later + let opt = msg.opt(); + + if let Some(opt) = opt.as_ref() { + writeln!(f, "\n;; OPT PSEUDOSECTION:")?; + writeln!( + f, + "; EDNS: version {}; flags: {}; udp: {}", + opt.version(), + opt.dnssec_ok(), + opt.udp_payload_size() + )?; + for option in opt.opt().iter::>() { + use AllOptData::*; + + match option { + Ok(opt) => match opt { + Nsid(nsid) => writeln!(f, "; NSID: {}", nsid)?, + Dau(dau) => writeln!(f, "; DAU: {}", dau)?, + Dhu(dhu) => writeln!(f, "; DHU: {}", dhu)?, + N3u(n3u) => writeln!(f, "; N3U: {}", n3u)?, + Expire(expire) => { + writeln!(f, "; EXPIRE: {}", expire)? + } + TcpKeepalive(opt) => { + writeln!(f, "; TCPKEEPALIVE: {}", opt)? + } + Padding(padding) => { + writeln!(f, "; PADDING: {}", padding)? + } + ClientSubnet(opt) => { + writeln!(f, "; CLIENTSUBNET: {}", opt)? + } + Cookie(cookie) => { + writeln!(f, "; COOKIE: {}", cookie)? + } + Chain(chain) => writeln!(f, "; CHAIN: {}", chain)?, + KeyTag(keytag) => { + writeln!(f, "; KEYTAG: {}", keytag)? + } + ExtendedError(extendederror) => { + writeln!(f, "; EDE: {}", extendederror)? + } + Other(other) => { + writeln!(f, "; {}", other.code())?; + } + }, + Err(err) => { + writeln!(f, "; ERROR: bad option: {}.", err)?; + } + } + } + } + + // Question + let questions = msg.question(); + if counts.qdcount() > 0 { + writeln!(f, ";; QUESTION SECTION:")?; + for item in questions { + if let Ok(item) = item { + writeln!(f, "; {}", item)?; + } else { + writeln!(f, "; ")?; + return Ok(()); + }; + } + } + + // Answer + let section = questions.answer().unwrap(); + if counts.ancount() > 0 { + writeln!(f, "\n;; ANSWER SECTION:")?; + for item in section { + if let Ok(item) = item { + write_record_item(f, &item)?; + } else { + writeln!(f, "; ")?; + return Ok(()); + }; + } + } + + // Authority + let section = section.next_section().unwrap().unwrap(); + if counts.nscount() > 0 { + writeln!(f, "\n;; AUTHORITY SECTION:")?; + for item in section { + if let Ok(item) = item { + write_record_item(f, &item)?; + } else { + writeln!(f, "; ")?; + return Ok(()); + }; + } + } + + // Additional + let section = section.next_section().unwrap().unwrap(); + if counts.arcount() > 1 || (opt.is_none() && counts.arcount() > 0) { + writeln!(f, "\n;; ADDITIONAL SECTION:")?; + for item in section { + if let Ok(item) = item { + if item.rtype() != Rtype::OPT { + write_record_item(f, &item)?; + } + } else { + writeln!(f, "; ")?; + return Ok(()); + }; + } + } + + Ok(()) + } +} + +fn write_record_item( + f: &mut impl fmt::Write, + item: &ParsedRecord<&[u8]>, +) -> Result<(), fmt::Error> { + let parsed = item.to_any_record::>(); + + match parsed { + Ok(item) => writeln!(f, "{}", item.display_zonefile(false)), + Err(_) => writeln!( + f, + "; {} {} {} {} ", + item.owner(), + item.ttl().as_secs(), + item.class(), + item.rtype() + ), + } +} diff --git a/src/base/iana/macros.rs b/src/base/iana/macros.rs index 5aa236a82..2c6d13908 100644 --- a/src/base/iana/macros.rs +++ b/src/base/iana/macros.rs @@ -13,6 +13,7 @@ macro_rules! int_enum { $value:expr, $mnemonic:expr) )* ) => { $(#[$attr])* #[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] + #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct $ianatype($inttype); impl $ianatype { diff --git a/src/base/message.rs b/src/base/message.rs index 32cae89f0..7fba1fae6 100644 --- a/src/base/message.rs +++ b/src/base/message.rs @@ -10,6 +10,7 @@ //! //! [`Message`]: struct.Message.html +use super::dig_printer::DigPrinter; use super::header::{Header, HeaderCounts, HeaderSection}; use super::iana::{Class, OptRcode, Rcode, Rtype}; use super::message_builder::{AdditionalBuilder, AnswerBuilder, PushError}; @@ -665,6 +666,20 @@ impl Message { } } +/// # Printing +impl> Message { + /// Create a wrapper that displays the message in a dig style + /// + /// The dig style resembles a zonefile format (see also [`ZonefileFmt`]), + /// with additional lines that are commented out that contain information + /// about the header, OPT record and more. + /// + /// [`ZonefileFmt`]: super::zonefile_fmt::ZonefileFmt + pub fn display_dig_style(&self) -> impl core::fmt::Display + '_ { + DigPrinter { msg: self } + } +} + //--- AsRef // Octs here can’t be ?Sized or it’ll conflict with AsRef<[u8]> below. diff --git a/src/base/mod.rs b/src/base/mod.rs index f242ad058..d543f4ec8 100644 --- a/src/base/mod.rs +++ b/src/base/mod.rs @@ -109,6 +109,7 @@ pub use self::serial::Serial; pub mod charstr; pub mod cmp; +mod dig_printer; pub mod header; pub mod iana; pub mod message; diff --git a/src/base/name/absolute.rs b/src/base/name/absolute.rs index 394521d05..bf8b20d8e 100644 --- a/src/base/name/absolute.rs +++ b/src/base/name/absolute.rs @@ -50,6 +50,7 @@ use std::vec::Vec; /// [`Display`]: std::fmt::Display #[derive(Clone)] #[repr(transparent)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct Name(Octs); impl Name<()> { diff --git a/src/base/zonefile_fmt.rs b/src/base/zonefile_fmt.rs index bef47ed56..8a9e22e75 100644 --- a/src/base/zonefile_fmt.rs +++ b/src/base/zonefile_fmt.rs @@ -32,8 +32,15 @@ impl fmt::Display for ZoneFileDisplay<'_, T> { /// Show a value as zonefile format pub trait ZonefileFmt { + /// Format the item as zonefile fmt into a [`fmt::Formatter`] + /// + /// This method is meant for use in a `fmt::Display` implementation. fn fmt(&self, p: &mut impl Formatter) -> Result; + /// Display the item as a zonefile + /// + /// The returned object will be displayed as zonefile when printed or + /// written using `fmt::Display`. fn display_zonefile(&self, pretty: bool) -> ZoneFileDisplay<'_, Self> { ZoneFileDisplay { inner: self, @@ -347,4 +354,21 @@ mod test { record.display_zonefile(false).to_string() ); } + + #[test] + fn naptr_record() { + use crate::rdata::Naptr; + let record = create_record(Naptr::, &Name<[u8]>>::new( + 100, + 50, + "a".parse().unwrap(), + "z3950+N2L+N2C".parse().unwrap(), + r#"!^urn:cid:.+@([^\\.]+\\.)(.*)$!\\2!i"#.parse().unwrap(), + Name::from_slice(b"\x09cidserver\x07example\x03com\x00").unwrap(), + )); + assert_eq!( + r#"example.com. 3600 IN NAPTR 100 50 "a" "z3950+N2L+N2C" "!^urn:cid:.+@([^\\.]+\\.)(.*)$!\\2!i" cidserver.example.com."#, + record.display_zonefile(false).to_string() + ); + } } diff --git a/src/net/server/single_service.rs b/src/net/server/single_service.rs index 64ef0aeff..28c6d19fe 100644 --- a/src/net/server/single_service.rs +++ b/src/net/server/single_service.rs @@ -13,10 +13,9 @@ use super::message::Request; use super::service::ServiceError; use crate::base::message_builder::AdditionalBuilder; use crate::base::opt::{AllOptData, ComposeOptData, LongOptData, OptRecord}; -use crate::base::{ - Message, MessageBuilder, Rtype, StreamTarget, UnknownRecordData, -}; +use crate::base::{Message, MessageBuilder, ParsedName, Rtype, StreamTarget}; use crate::dep::octseq::Octets; +use crate::rdata::AllRecordData; use std::boxed::Box; use std::future::Future; use std::pin::Pin; @@ -145,8 +144,8 @@ impl ComposeReply for ReplyMessage { for rr in &mut source { let rr = rr?; let rr = rr - .into_record::>()? - .expect("UnknownRecordData should not fail"); + .into_record::>>()? + .expect("AllRecordData should not fail"); target.push(rr).expect("push should not fail"); } @@ -157,8 +156,8 @@ impl ComposeReply for ReplyMessage { for rr in &mut source { let rr = rr?; let rr = rr - .into_record::>()? - .expect("UnknownRecordData should not fail"); + .into_record::>>()? + .expect("AllRecordData should not fail"); target.push(rr).expect("push should not fail"); } @@ -171,8 +170,8 @@ impl ComposeReply for ReplyMessage { if rr.rtype() == Rtype::OPT { } else { let rr = rr - .into_record::>()? - .expect("UnknownRecordData should not fail"); + .into_record::>>()? + .expect("AllRecordData should not fail"); target.push(rr).expect("push should not fail"); } } diff --git a/src/rdata/mod.rs b/src/rdata/mod.rs index c5b4828fc..1b3b4957c 100644 --- a/src/rdata/mod.rs +++ b/src/rdata/mod.rs @@ -105,6 +105,11 @@ rdata_types! { Ds, } } + naptr::{ + zone { + Naptr, + } + } nsec3::{ zone { Nsec3, diff --git a/src/rdata/naptr.rs b/src/rdata/naptr.rs new file mode 100644 index 000000000..44c8ba4bd --- /dev/null +++ b/src/rdata/naptr.rs @@ -0,0 +1,587 @@ +//! Record data from [RFC 3403]: NAPTR records. +//! +//! This RFC defines the NAPTR record type. +//! +//! [RFC 3403]: https://www.rfc-editor.org/info/rfc3403 + +use crate::base::{ + name::FlattenInto, + rdata::ComposeRecordData, + scan::{Scan, Scanner}, + wire::{Compose, Parse, ParseError}, + zonefile_fmt::{self, Formatter, ZonefileFmt}, + CanonicalOrd, CharStr, ParseRecordData, ParsedName, RecordData, Rtype, + ToName, +}; +use core::{cmp::Ordering, fmt, hash}; +#[cfg(feature = "serde")] +use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder}; +use octseq::{Octets, OctetsFrom, OctetsInto, Parser}; + +//------------ Naptr --------------------------------------------------------- + +/// Naptr record data. +/// +/// The Naptr encodes DNS rules for URI delegation, allowing changes and redelegation. +/// It uses regex for string-to-domain name conversion, chosen for compactness and +/// expressivity in small DNS packets. +/// +/// The Naptr record type is defined in [RFC 3403, section 4.1][1]. +/// +/// [1]: https://www.rfc-editor.org/rfc/rfc3403#section-4.1 +#[derive(Clone)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(bound( + serialize = " + Octs: octseq::serde::SerializeOctets + AsRef<[u8]>, + Name: serde::Serialize, + ", + deserialize = " + Octs: FromBuilder + octseq::serde::DeserializeOctets<'de>, + ::Builder: + OctetsBuilder + EmptyBuilder + + AsRef<[u8]> + AsMut<[u8]>, + Name: serde::Deserialize<'de>, + ", + )) +)] +pub struct Naptr { + order: u16, + preference: u16, + flags: CharStr, + services: CharStr, + regexp: CharStr, + replacement: Name, +} + +impl Naptr<(), ()> { + /// The rtype of this record data type. + pub(crate) const RTYPE: Rtype = Rtype::NAPTR; +} + +impl Naptr { + /// Creates a new Naptr record data from content. + pub fn new( + order: u16, + preference: u16, + flags: CharStr, + services: CharStr, + regexp: CharStr, + replacement: Name, + ) -> Self { + Naptr { + order, + preference, + flags, + services, + regexp, + replacement, + } + } + + /// The order of processing the records is from lowest to highest. + /// If two records have the same order value, they should be processed + /// according to their preference value and services field. + pub fn order(&self) -> u16 { + self.order + } + + /// The priority of the DDDS Algorithm, from lowest to highest. + pub fn preference(&self) -> u16 { + self.preference + } + + /// The flags controls aspects of the rewriting and interpretation of + /// the fields in the record. + pub fn flags(&self) -> &CharStr { + &self.flags + } + + /// The services specify the Service Parameters applicable to + /// this delegation path. + pub fn services(&self) -> &CharStr { + &self.services + } + + /// The regexp containing a substitution expression that is + /// applied to the original string held by the client in order to + /// construct the next domain name to lookup. + pub fn regexp(&self) -> &CharStr { + &self.regexp + } + + /// The replacement is the next domain name to query for, + /// depending on the potential values found in the flags field. + pub fn replacement(&self) -> &Name { + &self.replacement + } + + pub(in crate::rdata) fn convert_octets( + self, + ) -> Result, TOcts::Error> + where + TOcts: OctetsFrom, + TName: OctetsFrom, + { + Ok(Naptr::new( + self.order, + self.preference, + self.flags.try_octets_into()?, + self.services.try_octets_into()?, + self.regexp.try_octets_into()?, + self.replacement.try_octets_into()?, + )) + } + + pub(in crate::rdata) fn flatten( + self, + ) -> Result, TOcts::Error> + where + TOcts: OctetsFrom, + Name: FlattenInto, + { + Ok(Naptr::new( + self.order, + self.preference, + CharStr::try_octets_into(self.flags)?, + CharStr::try_octets_into(self.services)?, + CharStr::try_octets_into(self.regexp)?, + Name::try_flatten_into(self.replacement)?, + )) + } + + pub fn scan>( + scanner: &mut S, + ) -> Result { + Ok(Self::new( + u16::scan(scanner)?, + u16::scan(scanner)?, + scanner.scan_charstr()?, + scanner.scan_charstr()?, + scanner.scan_charstr()?, + scanner.scan_name()?, + )) + } +} + +impl> Naptr> { + pub fn parse<'a, Src: Octets = Octs> + ?Sized>( + parser: &mut octseq::Parser<'a, Src>, + ) -> Result { + Ok(Self::new( + u16::parse(parser)?, + u16::parse(parser)?, + CharStr::parse(parser)?, + CharStr::parse(parser)?, + CharStr::parse(parser)?, + ParsedName::parse(parser)?, + )) + } +} + +//--- OctetsFrom + +impl OctetsFrom> + for Naptr +where + Octs: OctetsFrom, + Name: OctetsFrom, +{ + type Error = Octs::Error; + + fn try_octets_from( + source: Naptr, + ) -> Result { + Ok(Naptr::new( + source.order, + source.preference, + CharStr::try_octets_from(source.flags)?, + CharStr::try_octets_from(source.services)?, + CharStr::try_octets_from(source.regexp)?, + Name::try_octets_from(source.replacement)?, + )) + } +} + +//--- FlattenInto + +impl FlattenInto> + for Naptr +where + TOcts: OctetsFrom, + Name: FlattenInto, +{ + type AppendError = TOcts::Error; + + fn try_flatten_into(self) -> Result, TOcts::Error> { + self.flatten() + } +} + +//--- PartialEq and Eq + +impl PartialEq> + for Naptr +where + Octs: AsRef<[u8]>, + OtherOcts: AsRef<[u8]>, + Name: ToName, + OtherName: ToName, +{ + fn eq(&self, other: &Naptr) -> bool { + self.order == other.order + && self.preference == other.preference + && self.flags.eq(&other.flags) + && self.services.eq(&other.services) + && self.regexp.eq(&other.regexp) + && self.replacement.name_eq(&other.replacement) + } +} + +impl, Name: ToName> Eq for Naptr {} + +//--- PartialOrd, Ord, and CanonicalOrd + +impl PartialOrd> + for Naptr +where + Octs: AsRef<[u8]>, + OtherOcts: AsRef<[u8]>, + Name: ToName, + OtherName: ToName, +{ + fn partial_cmp( + &self, + other: &Naptr, + ) -> Option { + match self.order.partial_cmp(&other.order) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.preference.partial_cmp(&other.preference) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.flags.partial_cmp(&other.flags) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.services.partial_cmp(&other.services) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.regexp.partial_cmp(&other.regexp) { + Some(Ordering::Equal) => {} + other => return other, + } + + Some(self.replacement.name_cmp(&other.replacement)) + } +} + +impl + CanonicalOrd> for Naptr +where + Octs: AsRef<[u8]>, + OtherOcts: AsRef<[u8]>, + Name: ToName, + OtherName: ToName, +{ + fn canonical_cmp(&self, other: &Naptr) -> Ordering { + match self.order.cmp(&other.order) { + Ordering::Equal => {} + other => return other, + } + match self.preference.cmp(&other.preference) { + Ordering::Equal => {} + other => return other, + } + match self.flags.canonical_cmp(&other.flags) { + Ordering::Equal => {} + other => return other, + } + match self.services.canonical_cmp(&other.services) { + Ordering::Equal => {} + other => return other, + } + match self.regexp.canonical_cmp(&other.regexp) { + Ordering::Equal => {} + other => return other, + } + + self.replacement.lowercase_composed_cmp(&other.replacement) + } +} + +impl Ord for Naptr +where + Octs: AsRef<[u8]>, + Name: ToName, +{ + fn cmp(&self, other: &Self) -> Ordering { + match self.order.cmp(&other.order) { + Ordering::Equal => {} + other => return other, + } + match self.preference.cmp(&other.preference) { + Ordering::Equal => {} + other => return other, + } + match self.flags.cmp(&other.flags) { + Ordering::Equal => {} + other => return other, + } + match self.services.cmp(&other.services) { + Ordering::Equal => {} + other => return other, + } + match self.regexp.cmp(&other.regexp) { + Ordering::Equal => {} + other => return other, + } + + self.replacement.name_cmp(&other.replacement) + } +} + +//--- Hash + +impl hash::Hash for Naptr +where + Octs: AsRef<[u8]>, + Name: hash::Hash, +{ + fn hash(&self, state: &mut H) { + self.order.hash(state); + self.preference.hash(state); + self.flags.hash(state); + self.services.hash(state); + self.regexp.hash(state); + self.replacement.hash(state); + } +} + +//--- RecordData, ParseRecordData, ComposeRecordData + +impl RecordData for Naptr { + fn rtype(&self) -> Rtype { + Naptr::RTYPE + } +} + +impl<'a, Octs: Octets + ?Sized> ParseRecordData<'a, Octs> + for Naptr, ParsedName>> +{ + fn parse_rdata( + rtype: Rtype, + parser: &mut Parser<'a, Octs>, + ) -> Result, ParseError> { + if rtype == Naptr::RTYPE { + Self::parse(parser).map(Some) + } else { + Ok(None) + } + } +} + +impl ComposeRecordData for Naptr +where + Octs: AsRef<[u8]>, + Name: ToName, +{ + fn rdlen(&self, _compress: bool) -> Option { + Some( + (u16::COMPOSE_LEN + u16::COMPOSE_LEN) + .checked_add(self.flags.compose_len()) + .expect("flags too long") + .checked_add(self.services.compose_len()) + .expect("services too long") + .checked_add(self.regexp.compose_len()) + .expect("regexp too long") + .checked_add(self.replacement.compose_len()) + .expect("replacement too long"), + ) + } + + fn compose_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + self.compose_head(target)?; + self.replacement.compose(target) + } + + fn compose_canonical_rdata< + Target: crate::base::wire::Composer + ?Sized, + >( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + self.compose_head(target)?; + self.replacement.compose_canonical(target) + } +} + +impl Naptr +where + Octs: AsRef<[u8]>, + Name: ToName, +{ + fn compose_head( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + self.order.compose(target)?; + self.preference.compose(target)?; + self.flags.compose(target)?; + self.services.compose(target)?; + self.regexp.compose(target) + } +} + +//--- Display + +impl core::fmt::Display for Naptr +where + Octs: AsRef<[u8]>, + Name: fmt::Display, +{ + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!( + f, + "{} {} {} {} {} {}.", + self.order, + self.preference, + self.flags.display_quoted(), + self.services.display_quoted(), + self.regexp.display_quoted(), + self.replacement + ) + } +} + +//--- Debug + +impl core::fmt::Debug for Naptr +where + Octs: AsRef<[u8]>, + Name: fmt::Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("Naptr") + .field("order", &self.order) + .field("preference", &self.preference) + .field("flags", &self.flags) + .field("services", &self.services) + .field("regexp", &self.regexp) + .field("replacement", &self.replacement) + .finish() + } +} + +//--- ZonefileFmt + +impl ZonefileFmt for Naptr +where + Octs: AsRef<[u8]>, + Name: ToName, +{ + fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { + p.block(|p| { + p.write_token(self.order)?; + p.write_comment("order")?; + p.write_token(self.preference)?; + p.write_comment("preference")?; + p.write_token(self.flags.display_quoted())?; + p.write_comment("flags")?; + p.write_token(self.services.display_quoted())?; + p.write_comment("services")?; + p.write_token(self.regexp.display_quoted())?; + p.write_comment("regexp")?; + p.write_token(self.replacement.fmt_with_dot())?; + p.write_comment("replacement") + }) + } +} + +//============ Testing ======================================================= + +#[cfg(test)] +#[cfg(all(feature = "std", feature = "bytes"))] +mod test { + use bytes::Bytes; + + use super::*; + use crate::base::{ + rdata::test::{test_compose_parse, test_rdlen, test_scan}, + Name, + }; + use core::str::FromStr; + use std::vec::Vec; + + #[test] + #[allow(clippy::redundant_closure)] // lifetimes ... + fn naptr_compose_parse_scan() { + let rdata = Naptr::new( + 100, + 50, + CharStr::from_octets("a").unwrap(), + CharStr::from_octets("z3950+N2L+N2C").unwrap(), + CharStr::from_octets("").unwrap(), + Name::>::from_str("cidserver.example.com.").unwrap(), + ); + test_rdlen(&rdata); + test_compose_parse(&rdata, |parser| Naptr::parse(parser)); + test_scan( + &[ + "100", + "50", + "a", + "z3950+N2L+N2C", + "", + "cidserver.example.com.", + ], + Naptr::scan, + &rdata, + ); + } + + #[test] + fn naptr_octets_into() { + let naptr: Naptr<&str, Name>> = Naptr::new( + 100, + 50, + CharStr::from_octets("a").unwrap(), + CharStr::from_octets("z3950+N2L+N2C").unwrap(), + CharStr::from_octets("").unwrap(), + Name::>::from_str("cidserver.example.com.").unwrap(), + ); + let naptr_bytes: Naptr> = + naptr.clone().octets_into(); + assert_eq!(naptr.order(), naptr_bytes.order()); + assert_eq!(naptr.preference(), naptr_bytes.preference()); + assert_eq!(naptr.flags(), naptr_bytes.flags()); + assert_eq!(naptr.services(), naptr_bytes.services()); + assert_eq!(naptr.regexp(), naptr_bytes.regexp()); + assert_eq!(naptr.replacement(), naptr_bytes.replacement()); + } + + #[test] + fn naptr_display() { + let naptr: Naptr<&str, Name>> = Naptr::new( + 100, + 50, + CharStr::from_octets("a").unwrap(), + CharStr::from_octets("z3950+N2L+N2C").unwrap(), + CharStr::from_octets(r#"!^urn:cid:.+@([^\.]+\.)(.*)$!\2!i"#) + .unwrap(), + Name::>::from_str("cidserver.example.com.").unwrap(), + ); + assert_eq!( + format!("{}", naptr), + r#"100 50 "a" "z3950+N2L+N2C" "!^urn:cid:.+@([^\\.]+\\.)(.*)$!\\2!i" cidserver.example.com."# + ); + } +} diff --git a/src/rdata/nsec3.rs b/src/rdata/nsec3.rs index a09e4c309..d80f12b9b 100644 --- a/src/rdata/nsec3.rs +++ b/src/rdata/nsec3.rs @@ -752,6 +752,7 @@ impl> ZonefileFmt for Nsec3param { /// no whitespace allowed. #[derive(Clone)] #[repr(transparent)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct Nsec3Salt(Octs); impl Nsec3Salt<()> { diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 008d6933f..61549965b 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -20,7 +20,7 @@ //! ``` //! # use domain::sign::*; //! # use domain::base::Name; -//! // Generate a new ED25519 key. +//! // Generate a new Ed25519 key. //! let params = GenerateParams::Ed25519; //! let (sec_bytes, pub_bytes) = common::generate(params).unwrap(); //! @@ -41,6 +41,31 @@ //! println!("{:?}", sig); //! ``` //! +//! It is also possible to import keys stored on disk in the conventional BIND +//! format. +//! +//! ``` +//! # use domain::base::iana::SecAlg; +//! # use domain::{sign::*, validate}; +//! // Load an Ed25519 key named 'Ktest.+015+56037'. +//! let base = "test-data/dnssec-keys/Ktest.+015+56037"; +//! let sec_text = std::fs::read_to_string(format!("{base}.private")).unwrap(); +//! let sec_bytes = SecretKeyBytes::parse_from_bind(&sec_text).unwrap(); +//! let pub_text = std::fs::read_to_string(format!("{base}.key")).unwrap(); +//! let pub_key = validate::Key::>::parse_from_bind(&pub_text).unwrap(); +//! +//! // Parse the key into Ring or OpenSSL. +//! let key_pair = common::KeyPair::from_bytes(&sec_bytes, pub_key.raw_public_key()).unwrap(); +//! +//! // Associate the key with important metadata. +//! let key = SigningKey::new(pub_key.owner().clone(), pub_key.flags(), key_pair); +//! +//! // Check that the owner, algorithm, and key tag matched expectations. +//! assert_eq!(key.owner().to_string(), "test"); +//! assert_eq!(key.algorithm(), SecAlg::ED25519); +//! assert_eq!(key.public_key().key_tag(), 56037); +//! ``` +//! //! # Cryptography //! //! This crate supports OpenSSL and Ring for performing cryptography. These @@ -62,13 +87,8 @@ //! While each cryptographic backend can support a limited number of signature //! algorithms, even the types independent of a cryptographic backend (e.g. //! [`SecretKeyBytes`] and [`GenerateParams`]) support a limited number of -//! algorithms. They are: -//! -//! - RSA/SHA-256 -//! - ECDSA P-256/SHA-256 -//! - ECDSA P-384/SHA-384 -//! - Ed25519 -//! - Ed448 +//! algorithms. Even with custom cryptographic backends, this module can only +//! support these algorithms. //! //! # Importing and Exporting //! @@ -96,9 +116,11 @@ use core::fmt; use crate::{ base::{iana::SecAlg, Name}, - validate::{self, PublicKeyBytes, Signature}, + validate, }; +pub use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; + mod bytes; pub use self::bytes::{RsaSecretKeyBytes, SecretKeyBytes}; @@ -235,8 +257,7 @@ impl SigningKey { /// Low-level signing functionality. /// /// Types that implement this trait own a private key and can sign arbitrary -/// information (for zone signing keys, DNS records; for key signing keys, -/// subsidiary public keys). +/// information (in the form of slices of bytes). /// /// Implementing types should validate keys during construction, so that /// signing does not fail due to invalid keys. If the implementing type diff --git a/src/sign/records.rs b/src/sign/records.rs index b1fd3d50e..2a79f1440 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -359,6 +359,10 @@ impl SortedRecords { } let mut bitmap = RtypeBitmap::::builder(); + // RFC 4035 section 2.3: + // "The type bitmap of every NSEC resource record in a signed + // zone MUST indicate the presence of both the NSEC record + // itself and its corresponding RRSIG record." bitmap.add(Rtype::RRSIG).unwrap(); if assume_dnskeys_will_be_added && family.owner() == &apex_owner { // Assume there's gonna be a DNSKEY. diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 7ff8fb574..d1e29c395 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -18,8 +18,10 @@ use ring::signature::{ }; use secrecy::ExposeSecret; -use crate::base::iana::SecAlg; -use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; +use crate::{ + base::iana::SecAlg, + validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}, +}; use super::{GenerateParams, SecretKeyBytes, SignError, SignRaw}; diff --git a/src/validate.rs b/src/validate.rs index 3293df0f0..cdcc18312 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -100,6 +100,11 @@ impl Key { self.key.algorithm() } + /// The size of this key, in bits. + pub fn key_size(&self) -> usize { + self.key.key_size() + } + /// Whether this is a zone signing key. /// /// From [RFC 4034, section 2.1.1]: @@ -420,6 +425,27 @@ impl PublicKeyBytes { } } + /// The size of this key, in bits. + /// + /// For RSA keys, this measures the size of the public modulus. For all + /// other algorithms, it is the size of the fixed-width public key. + pub fn key_size(&self) -> usize { + match self { + Self::RsaSha1(k) + | Self::RsaSha1Nsec3Sha1(k) + | Self::RsaSha256(k) + | Self::RsaSha512(k) => k.key_size(), + + // ECDSA public keys have a marker byte and two points. + Self::EcdsaP256Sha256(k) => (k.len() - 1) / 2 * 8, + Self::EcdsaP384Sha384(k) => (k.len() - 1) / 2 * 8, + + // EdDSA public key sizes are measured in encoded form. + Self::Ed25519(k) => k.len() * 8, + Self::Ed448(k) => k.len() * 8, + } + } + /// The raw key tag computation for this value. fn raw_key_tag(&self) -> u32 { fn compute(data: &[u8]) -> u32 { @@ -583,6 +609,11 @@ pub struct RsaPublicKeyBytes { //--- Inspection impl RsaPublicKeyBytes { + /// The size of the public modulus, in bits. + pub fn key_size(&self) -> usize { + self.n.len() * 8 - self.n[0].leading_zeros() as usize + } + /// The raw key tag computation for this value. fn raw_key_tag(&self) -> u32 { let mut res = 0u32; @@ -658,13 +689,18 @@ impl RsaPublicKeyBytes { }; // NOTE: off <= 3 so is safe to index up to. - let e = data[off..] + let e: Box<[u8]> = data[off..] .get(..exp_len) .ok_or(FromDnskeyError::InvalidKey)? .into(); // NOTE: The previous statement indexed up to 'exp_len'. - let n = data[off + exp_len..].into(); + let n: Box<[u8]> = data[off + exp_len..].into(); + + // Empty values and leading zeros are not allowed. + if e.is_empty() || n.is_empty() || e[0] == 0 || n[0] == 0 { + return Err(FromDnskeyError::InvalidKey); + } Ok(Self { n, e }) } @@ -1256,14 +1292,14 @@ mod test { type Dnskey = crate::rdata::Dnskey>; type Rrsig = crate::rdata::Rrsig, Name>; - const KEYS: &[(SecAlg, u16)] = &[ - (SecAlg::RSASHA1, 439), - (SecAlg::RSASHA1_NSEC3_SHA1, 22204), - (SecAlg::RSASHA256, 60616), - (SecAlg::ECDSAP256SHA256, 42253), - (SecAlg::ECDSAP384SHA384, 33566), - (SecAlg::ED25519, 56037), - (SecAlg::ED448, 7379), + const KEYS: &[(SecAlg, u16, usize)] = &[ + (SecAlg::RSASHA1, 439, 2048), + (SecAlg::RSASHA1_NSEC3_SHA1, 22204, 2048), + (SecAlg::RSASHA256, 60616, 2048), + (SecAlg::ECDSAP256SHA256, 42253, 256), + (SecAlg::ECDSAP384SHA384, 33566, 384), + (SecAlg::ED25519, 56037, 256), + (SecAlg::ED448, 7379, 456), ]; // Returns current root KSK/ZSK for testing (2048b) @@ -1312,7 +1348,7 @@ mod test { #[test] fn parse_from_bind() { - for &(algorithm, key_tag) in KEYS { + for &(algorithm, key_tag, _) in KEYS { let name = format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); @@ -1322,9 +1358,22 @@ mod test { } } + #[test] + fn key_size() { + for &(algorithm, key_tag, key_size) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = Key::>::parse_from_bind(&data).unwrap(); + assert_eq!(key.key_size(), key_size); + } + } + #[test] fn key_tag() { - for &(algorithm, key_tag) in KEYS { + for &(algorithm, key_tag, _) in KEYS { let name = format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); @@ -1338,7 +1387,7 @@ mod test { #[test] fn digest() { - for &(algorithm, key_tag) in KEYS { + for &(algorithm, key_tag, _) in KEYS { let name = format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); @@ -1361,7 +1410,7 @@ mod test { #[test] fn dnskey_roundtrip() { - for &(algorithm, key_tag) in KEYS { + for &(algorithm, key_tag, _) in KEYS { let name = format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); @@ -1376,7 +1425,7 @@ mod test { #[test] fn bind_format_roundtrip() { - for &(algorithm, key_tag) in KEYS { + for &(algorithm, key_tag, _) in KEYS { let name = format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); @@ -1712,6 +1761,27 @@ pub enum Nsec3HashError { CollisionDetected, } +///--- Display + +impl std::fmt::Display for Nsec3HashError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Nsec3HashError::UnsupportedAlgorithm => { + f.write_str("Unsupported algorithm") + } + Nsec3HashError::AppendError => { + f.write_str("Append error: out of memory?") + } + Nsec3HashError::OwnerHashError => { + f.write_str("Hashing produced an invalid owner hash") + } + Nsec3HashError::CollisionDetected => { + f.write_str("Hash collision detected") + } + } + } +} + /// Compute an [RFC 5155] NSEC3 hash using default settings. /// /// See: [Nsec3param::default].