From ff035e478e304a3ccb4e365d23161a821fdc2692 Mon Sep 17 00:00:00 2001 From: Weilence Date: Tue, 26 Nov 2024 22:23:50 +0800 Subject: [PATCH] Add support for CAA record type --- src/base/charstr.rs | 16 ++ src/base/zonefile_fmt.rs | 14 ++ src/rdata/caa.rs | 398 +++++++++++++++++++++++++++++++++++++++ src/rdata/mod.rs | 5 + 4 files changed, 433 insertions(+) create mode 100644 src/rdata/caa.rs diff --git a/src/base/charstr.rs b/src/base/charstr.rs index b47743c33..cf9344acc 100644 --- a/src/base/charstr.rs +++ b/src/base/charstr.rs @@ -380,6 +380,22 @@ impl + ?Sized> CharStr { .compose(target)?; target.append_slice(self.0.as_ref()) } + + /// Appends the canonical wire format representation to an octets builder. + pub fn compose_canonical( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + u8::try_from(self.0.as_ref().len()) + .expect("long charstr") + .compose(target)?; + + for ch in self.0.as_ref() { + ch.compose(target)?; + } + + Ok(()) + } } impl CharStr { diff --git a/src/base/zonefile_fmt.rs b/src/base/zonefile_fmt.rs index 8a9e22e75..1a2aeb362 100644 --- a/src/base/zonefile_fmt.rs +++ b/src/base/zonefile_fmt.rs @@ -371,4 +371,18 @@ mod test { record.display_zonefile(false).to_string() ); } + + #[test] + fn caa_record() { + use crate::rdata::Caa; + let record = create_record(Caa::new( + 0, + "issue".parse().unwrap(), + "ca.example.net".as_bytes().to_vec(), + )); + assert_eq!( + "example.com. 3600 IN CAA 0 issue \"ca.example.net\"", + record.display_zonefile(false).to_string() + ); + } } diff --git a/src/rdata/caa.rs b/src/rdata/caa.rs new file mode 100644 index 000000000..a830dd75e --- /dev/null +++ b/src/rdata/caa.rs @@ -0,0 +1,398 @@ +//! Record data from [RFC 8659]: CAA records. +//! +//! This RFC defines the CAA record type. +//! +//! [RFC 8659]: https://www.rfc-editor.org/info/rfc8659 + +use crate::base::{ + name::FlattenInto, + rdata::ComposeRecordData, + scan::{Scan, Scanner}, + wire::{Compose, Parse, ParseError}, + zonefile_fmt::{self, Formatter, ZonefileFmt}, + CanonicalOrd, CharStr, ParseRecordData, RecordData, Rtype, +}; +use core::{cmp::Ordering, fmt, hash}; +#[cfg(feature = "serde")] +use octseq::{ + builder::{EmptyBuilder, FromBuilder, OctetsBuilder}, + serde::DeserializeOctets, + serde::SerializeOctets, +}; +use octseq::{Octets, OctetsFrom, OctetsInto, Parser}; + +//------------ Caa --------------------------------------------------------- + +/// Caa record data. +/// +/// The Certification Authority Authorization (CAA) DNS Resource Record allows +/// a DNS domain name holder to specify one or more Certification Authorities +/// (CAs) authorized to issue certificates for that domain name. +/// +/// CAA Resource Records allow a public CA to implement additional controls to reduce the +/// risk of unintended certificate mis-issue. +/// +/// The Caa record type is defined in [RFC 8659, section 4.1][1]. +/// +/// [1]: https://www.rfc-editor.org/rfc/rfc8659#section-4.1 +#[derive(Clone)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(bound( + serialize = " + Octs: SerializeOctets + AsRef<[u8]> + ", + deserialize = " + Octs: FromBuilder + DeserializeOctets<'de>, + ::Builder: + OctetsBuilder + EmptyBuilder + + AsRef<[u8]>, + ", + )) +)] +pub struct Caa { + flags: u8, + tag: CharStr, + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "octseq::serde::SerializeOctets::serialize_octets", + deserialize_with = "octseq::serde::DeserializeOctets::deserialize_octets", + bound( + serialize = "Octs: octseq::serde::SerializeOctets", + deserialize = "Octs: octseq::serde::DeserializeOctets<'de>", + ) + ) + )] + value: Octs, +} + +impl Caa<()> { + // The rtype of this record data type. + pub const RTYPE: Rtype = Rtype::CAA; +} + +impl Caa { + /// Creates a new CAA record data from the flags, tag, and value. + pub fn new(flags: u8, tag: CharStr, value: Octs) -> Self { + Caa { flags, tag, value } + } + + /// If the value is set to "1", the Property is critical. + /// A CA MUST NOT issue certificates for any FQDN if the + /// Relevant RRset for that FQDN contains a CAA critical + /// Property for an unknown or unsupported Property Tag. + pub fn flags(&self) -> u8 { + self.flags + } + + /// The Property identifier + pub fn tag(&self) -> &CharStr { + &self.tag + } + + /// The Property Value + pub fn value(&self) -> &Octs { + &self.value + } + + pub(in crate::rdata) fn convert_octets>( + self, + ) -> Result, TOcts::Error> { + Ok(Caa::new( + self.flags, + self.tag.try_octets_into()?, + self.value.try_octets_into()?, + )) + } + + pub(in crate::rdata) fn flatten>( + self, + ) -> Result, TOcts::Error> { + self.convert_octets() + } + + pub fn scan>( + scanner: &mut S, + ) -> Result { + Ok(Self::new( + u8::scan(scanner)?, + CharStr::scan(scanner)?, + scanner.scan_octets()?, + )) + } + + pub fn parse<'a, Src: Octets = Octs> + ?Sized>( + parser: &mut Parser<'a, Src>, + ) -> Result { + Ok(Self::new( + u8::parse(parser)?, + CharStr::parse(parser)?, + parser.parse_octets(parser.remaining())?, + )) + } +} + +//--- OctetsFrom + +impl OctetsFrom> for Caa +where + Octs: OctetsFrom, +{ + type Error = Octs::Error; + + fn try_octets_from(source: Caa) -> Result { + Ok(Caa { + flags: source.flags, + tag: CharStr::try_octets_from(source.tag)?, + value: Octs::try_octets_from(source.value)?, + }) + } +} + +//--- FlattenInto + +impl FlattenInto> for Caa +where + TOcts: OctetsFrom, +{ + type AppendError = TOcts::Error; + + fn try_flatten_into(self) -> Result, Self::AppendError> { + self.flatten() + } +} + +//--- PartialEq and Eq + +impl PartialEq> for Caa +where + Octs: AsRef<[u8]>, + OtherOcts: AsRef<[u8]>, +{ + fn eq(&self, other: &Caa) -> bool { + self.flags == other.flags + && self.tag.eq(&other.tag) + && self.value.as_ref().eq(other.value.as_ref()) + } +} + +impl> Eq for Caa {} + +//--- PartialOrd, Ord, and CanonicalOrd + +impl PartialOrd> for Caa +where + Octs: AsRef<[u8]>, + OtherOcts: AsRef<[u8]>, +{ + fn partial_cmp(&self, other: &Caa) -> Option { + match self.flags.partial_cmp(&other.flags) { + Some(Ordering::Equal) => (), + other => return other, + } + match self.tag.partial_cmp(&other.tag) { + Some(Ordering::Equal) => (), + other => return other, + } + self.value.as_ref().partial_cmp(other.value.as_ref()) + } +} + +impl CanonicalOrd> for Caa +where + Octs: AsRef<[u8]>, + OtherOcts: AsRef<[u8]>, +{ + fn canonical_cmp(&self, other: &Caa) -> Ordering { + match self.flags.cmp(&other.flags) { + Ordering::Equal => (), + ord => return ord, + } + match self.tag.canonical_cmp(&other.tag) { + Ordering::Equal => (), + ord => return ord, + } + self.value.as_ref().cmp(other.value.as_ref()) + } +} + +impl> Ord for Caa { + fn cmp(&self, other: &Self) -> Ordering { + match self.flags.cmp(&other.flags) { + Ordering::Equal => (), + ord => return ord, + } + match self.tag.cmp(&other.tag) { + Ordering::Equal => (), + ord => return ord, + } + self.value.as_ref().cmp(other.value.as_ref()) + } +} + +//--- Hash + +impl> hash::Hash for Caa { + fn hash(&self, state: &mut H) { + self.flags.hash(state); + self.tag.hash(state); + self.value.as_ref().hash(state); + } +} + +//--- RecordData, ParseRecordData, ComposeRecordData + +impl RecordData for Caa { + fn rtype(&self) -> Rtype { + Caa::RTYPE + } +} + +impl<'a, Octs: Octets + ?Sized> ParseRecordData<'a, Octs> + for Caa> +{ + fn parse_rdata( + rtype: Rtype, + parser: &mut octseq::Parser<'a, Octs>, + ) -> Result, crate::base::wire::ParseError> { + if rtype == Caa::RTYPE { + Self::parse(parser).map(Some) + } else { + Ok(None) + } + } +} + +impl> ComposeRecordData for Caa +where + Octs: AsRef<[u8]>, +{ + fn rdlen(&self, _compress: bool) -> Option { + Some( + u8::COMPOSE_LEN + .checked_add(self.tag.compose_len()) + .expect("long tag") + .checked_add( + u16::try_from(self.value.as_ref().len()) + .expect("long value"), + ) + .expect("long value"), + ) + } + + fn compose_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + self.flags.compose(target)?; + self.tag.compose(target)?; + target.append_slice(self.value.as_ref()) + } + + fn compose_canonical_rdata< + Target: crate::base::wire::Composer + ?Sized, + >( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + self.flags.compose(target)?; + self.tag.compose_canonical(target)?; + target.append_slice(self.value.as_ref()) + } +} + +//--- Display + +impl> fmt::Display for Caa { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{} {} {}", + self.flags, + self.tag, + unsafe { CharStr::from_octets_unchecked(&self.value) } + .display_quoted() + ) + } +} + +//--- Debug + +impl> fmt::Debug for Caa { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Caa") + .field("flags", &self.flags) + .field("tag", &self.tag) + .field("value", &unsafe { + CharStr::from_octets_unchecked(&self.value) + }) + .finish() + } +} + +//--- ZonefileFmt + +impl> ZonefileFmt for Caa { + fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { + p.block(|p| { + p.write_token(self.flags)?; + p.write_comment("flags")?; + p.write_token(&self.tag)?; + p.write_comment("tag")?; + p.write_token( + unsafe { CharStr::from_octets_unchecked(&self.value) } + .display_quoted(), + )?; + p.write_comment("value") + }) + } +} + +#[cfg(test)] +#[cfg(all(feature = "std", feature = "bytes"))] +mod test { + use super::*; + use crate::std::string::ToString; + use std::vec::Vec; + + #[test] + fn caa_eq() { + let caa1: Caa> = Caa::new( + 0, + "ISSUE".parse().unwrap(), + "ca.example.net".as_bytes().to_vec(), + ); + let caa2: Caa> = Caa::new( + 0, + "issue".parse().unwrap(), + "ca.example.net".as_bytes().to_vec(), + ); + assert_eq!(caa1, caa2); + } + + #[test] + fn caa_octets_info() { + let caa: Caa> = Caa::new( + 0, + "issue".parse().unwrap(), + "ca.example.net".as_bytes().to_vec(), + ); + let caa_bytes: Caa = caa.clone().octets_into(); + assert_eq!(caa.flags, caa_bytes.flags); + assert_eq!(caa.tag, caa_bytes.tag); + assert_eq!(caa.value, caa_bytes.value); + } + + #[test] + fn caa_display() { + let caa: Caa> = Caa::new( + 0, + "issue".parse().unwrap(), + "ca.example.net".as_bytes().to_vec(), + ); + + assert_eq!(caa.to_string(), r#"0 issue "ca.example.net""#); + } +} diff --git a/src/rdata/mod.rs b/src/rdata/mod.rs index 1b3b4957c..c945cc7cd 100644 --- a/src/rdata/mod.rs +++ b/src/rdata/mod.rs @@ -86,6 +86,11 @@ rdata_types! { Aaaa, } } + caa::{ + zone { + Caa, + } + } cds::{ zone { Cdnskey,