diff --git a/src/base/iana/ipseckey.rs b/src/base/iana/ipseckey.rs new file mode 100644 index 00000000..0dc85c84 --- /dev/null +++ b/src/base/iana/ipseckey.rs @@ -0,0 +1,71 @@ +//! IPSECKEY IANA parameters. +//! +//! The values of these types don't officially have an IANA assigned name and +//! mnemonic. For ease of use, we define them here anyway. + +//------------ IpseckeyAlgorithm --------------------------------------------- + +int_enum! { + /// IPSECKEY Algorithms. + /// + /// This type identifies the public key's cryptographic algorithm of the + /// [IPSECKEY]. + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2025-09-09. + /// + /// [IPSECKEY]: ../../../rdata/ipseckey/index.html + /// [IANA registration]: https://www.iana.org/assignments/ipseckey-rr-parameters/ipseckey-rr-parameters.xhtml#ipseckey-rr-parameters-1 + => + IpseckeyAlgorithm, u8; + + /// Specified that no Public key is present. + (NONE => 0, "NONE") + + /// Specified that a DSA Public Key is used. + (DSA => 1, "DSA") + + /// Specified that an RSA Public Key is used. + (RSA => 2, "RSA") + + /// Specified that an ECDSA Public Key is used. + (ECDSA => 3, "ECDSA") + + /// Specified that an EdDSA Public Key is used. + (EDDSA => 4, "EdDSA") +} + +int_enum_str_decimal!(IpseckeyAlgorithm, u8); +int_enum_zonefile_fmt_decimal!(IpseckeyAlgorithm, "ipseckey algorithm"); + +//------------ IpseckeyGateway ----------------------------------------------- + +int_enum! { + /// IPSECKEY Gateway Types. + /// + /// This type indicates the format of the information that is stored in + /// the gateway field of the [IPSECKEY]. + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2025-09-09. + /// + /// [IPSECKEY]: ../../../rdata/ipseckey/index.html + /// [IANA registration]: https://www.iana.org/assignments/ipseckey-rr-parameters/ipseckey-rr-parameters.xhtml#ipseckey-rr-parameters-2 + => + IpseckeyGatewayType, u8; + + /// Specified that No gateway is present. + (NONE => 0, "NONE") + + /// Specified that A 4-byte IPv4 address is present. + (IPV4 => 1, "IPV4") + + /// Specified that A 16-byte IPv6 address is present. + (IPV6 => 2, "IPV6") + + /// Specified that A wire-encoded domain name is present. + (NAME => 3, "NAME") +} + +int_enum_str_decimal!(IpseckeyGatewayType, u8); +int_enum_zonefile_fmt_decimal!(IpseckeyGatewayType, "ipseckey gateway type"); diff --git a/src/base/iana/mod.rs b/src/base/iana/mod.rs index 1eee9dc8..1c734cce 100644 --- a/src/base/iana/mod.rs +++ b/src/base/iana/mod.rs @@ -28,13 +28,16 @@ pub use self::class::Class; pub use self::digestalg::DigestAlgorithm; pub use self::exterr::ExtendedErrorCode; +pub use self::ipseckey::{IpseckeyAlgorithm, IpseckeyGatewayType}; pub use self::nsec3::Nsec3HashAlgorithm; pub use self::opcode::Opcode; pub use self::opt::OptionCode; pub use self::rcode::{OptRcode, Rcode, TsigRcode}; pub use self::rtype::Rtype; pub use self::secalg::SecurityAlgorithm; +pub use self::sshfp::{SshfpAlgorithm, SshfpType}; pub use self::svcb::SvcParamKey; +pub use self::tlsa::{TlsaCertificateUsage, TlsaMatchingType, TlsaSelector}; pub use self::zonemd::{ZonemdAlgorithm, ZonemdScheme}; #[macro_use] @@ -43,11 +46,14 @@ mod macros; pub mod class; pub mod digestalg; pub mod exterr; +pub mod ipseckey; pub mod nsec3; pub mod opcode; pub mod opt; pub mod rcode; pub mod rtype; pub mod secalg; +pub mod sshfp; pub mod svcb; +pub mod tlsa; pub mod zonemd; diff --git a/src/base/iana/sshfp.rs b/src/base/iana/sshfp.rs new file mode 100644 index 00000000..22675a7f --- /dev/null +++ b/src/base/iana/sshfp.rs @@ -0,0 +1,75 @@ +//! SSHFP IANA parameters. +//! +//! [RFC 4255]: https://tools.ietf.org/html/rfc4255 +//! [RFC 6594]: https://tools.ietf.org/html/rfc6594 +//! [RFC 7479]: https://tools.ietf.org/html/rfc7479 +//! [RFC 8709]: https://tools.ietf.org/html/rfc8709 +//! +//! The values of these types don't officially have an IANA assigned mnemonic. +//! For ease of use, we define them here anyway. + +//------------ SshfpType ----------------------------------------------------- + +int_enum! { + /// SSHFP fingerprint type. + /// + /// This type selects the digest algorithm used for the fingerprint in the + /// [SSHFP] record. + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2025-09-04. + /// + /// [SSHFP]: ../../../rdata/sshfp/index.html + /// [IANA registration]: https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml#dns-sshfp-rr-parameters-2 + => + SshfpType, u8; + + (RESERVED => 0, "Reserved") + + /// Specified that the SHA-1 algorithm is used. [RFC4255] + (SHA1 => 1, "SHA-1") + + /// Specified that the SHA-256 algorithm is used. [RFC6594] + (SHA256 => 2, "SHA-256") + +} + +int_enum_str_decimal!(SshfpType, u8); +int_enum_zonefile_fmt_decimal!(SshfpType, "fingerprint type"); + +//------------ SshfpAlgorithm ------------------------------------------------ + +int_enum! { + /// SSHFP public key algorithms. + /// + /// This type selects the algorithm of the public key associated with the [SSHFP]. + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2025-09-04. + /// + /// [SSHFP]: ../../../rdata/sshfp/index.html + /// [IANA registration]: https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml#dns-sshfp-rr-parameters-1 + => + SshfpAlgorithm, u8; + + /// Specified that the Reserved algorithm is used. [RFC4255] + (RESERVED => 0, "Reserved") + + /// Specified that the RSA algorithm is used. [RFC4255] + (RSA => 1, "RSA") + + /// Specified that the DSA algorithm is used. [RFC4255] + (DSA => 2, "DSA") + + /// Specified that the ECDSA algorithm is used. [RFC6594] + (ECDSA => 3, "ECDSA") + + /// Specified that the Ed25519 algorithm is used. [RFC7479] + (ED25519 => 4, "Ed25519") + + /// Specified that the Ed448 algorithm is used. [RFC8709] + (ED448 => 6, "Ed448") +} + +int_enum_str_decimal!(SshfpAlgorithm, u8); +int_enum_zonefile_fmt_decimal!(SshfpAlgorithm, "public key algorithm"); diff --git a/src/base/iana/svcb.rs b/src/base/iana/svcb.rs index 9dd31fbe..58e651d2 100644 --- a/src/base/iana/svcb.rs +++ b/src/base/iana/svcb.rs @@ -12,8 +12,12 @@ int_enum! { // https://datatracker.ietf.org/doc/draft-ietf-tls-esni/ (ECH => 5, "ech") (IPV6HINT => 6, "ipv6hint") - // https://datatracker.ietf.org/doc/draft-ietf-add-svcb-dns/ + // https://datatracker.ietf.org/doc/rfc9461/ (DOHPATH => 7, "dohpath") + (OHTTP => 8, "ohttp") + // https://datatracker.ietf.org/doc/draft-ietf-tls-key-share-prediction/ + (TLS_SUPPORTED_GROUPS => 9, "tls-supported-groups") + // TODO: docpath https://datatracker.ietf.org/doc/draft-ietf-core-dns-over-coap/ } int_enum_str_with_prefix!(SvcParamKey, "key", b"key", u16, "unknown key"); diff --git a/src/base/iana/tlsa.rs b/src/base/iana/tlsa.rs new file mode 100644 index 00000000..b320e4c2 --- /dev/null +++ b/src/base/iana/tlsa.rs @@ -0,0 +1,99 @@ +//! TLSA IANA parameters. + +//------------ TlsaCertificateUsage ------------------------------------------ + +int_enum! { + /// TLSA Certificate Usage type. + /// + /// This type specifies the provided association that will be used to match the certificate + /// presented in the TLS handshake + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2025-09-04. + /// + /// [TLSA]: ../../../rdata/tlsa/index.html + /// [IANA registration]: https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml#certificate-usages + => + TlsaCertificateUsage, u8; + + /// CA constraint + (PKIX_TA => 0, "PKIX-TA") + + /// Service certificate constraint + (PKIX_EE => 1, "PKIX-EE") + + /// Trust anchor assertion + (DANE_TA => 2, "DANE-TA") + + /// Domain-issued certificate + (DANE_EE => 3, "DANE-EE") + + /// Reserved for Private Use + (PRIVCERT => 255, "PrivCert") +} + +int_enum_str_decimal!(TlsaCertificateUsage, u8); +int_enum_zonefile_fmt_decimal!( + TlsaCertificateUsage, + "certificate usage type" +); + +//------------ TlsaSelector -------------------------------------------------- + +int_enum! { + /// TLSA Selector type. + /// + /// This type specifies which part of the TLS certificate presented by the server will be + /// matched against the association data + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2025-09-04. + /// + /// [TLSA]: ../../../rdata/tlsa/index.html + /// [IANA registration]: https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml#selectors + => + TlsaSelector, u8; + + /// Full certificate + (CERT => 0, "Cert") + + /// SubjectPublicKeyInfo + (SPKI => 1, "SPKI") + + /// Reserved for Private Use + (PRIVSEL => 255, "PrivSel") +} + +int_enum_str_decimal!(TlsaSelector, u8); +int_enum_zonefile_fmt_decimal!(TlsaSelector, "selector"); + +//------------ TlsaMatchingType ---------------------------------------------- + +int_enum! { + /// TLSA Matching Type type. + /// + /// This type specifies how the certificate association is presented. + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2025-09-04. + /// + /// [TLSA]: ../../../rdata/tlsa/index.html + /// [IANA registration]: https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml#matching-types + => + TlsaMatchingType, u8; + + /// No hash used + (FULL => 0, "Full") + + /// 256 bit hash by SHA2 + (SHA2_256 => 1, "SHA2-256") + + /// 512 bit hash by SHA2 + (SHA2_512 => 2, "SHA2-512") + + /// Reserved for Private Use + (PRIVMATCH => 255, "PrivMatch") +} + +int_enum_str_decimal!(TlsaMatchingType, u8); +int_enum_zonefile_fmt_decimal!(TlsaMatchingType, "matching type"); diff --git a/src/base/scan.rs b/src/base/scan.rs index df242082..f7ae29fd 100644 --- a/src/base/scan.rs +++ b/src/base/scan.rs @@ -211,6 +211,18 @@ pub trait Scanner { /// It can be of any length. fn scan_octets(&mut self) -> Result; + /// Scans a token into an octets sequence combining tokens that are not + /// separated by whitespace into a single token (used for SVCB quoted + /// SvcParamValues). + /// + /// The returned sequence has all symbols converted into their octets. + /// It can be of any length. + fn scan_svcb_octets(&mut self) -> Result { + Err(Self::Error::custom( + "Scanning SVCB octets is only implemented by some Scanners", + )) + } + /// Scans a token as a borrowed ASCII string. /// /// If the next token contains non-ascii characters, returns an error. diff --git a/src/rdata/ipseckey.rs b/src/rdata/ipseckey.rs new file mode 100644 index 00000000..435b3bbc --- /dev/null +++ b/src/rdata/ipseckey.rs @@ -0,0 +1,922 @@ +//! IPSECKEY record data. +//! +//! The IPSECKEY Resource Record is used to publish a public key that is to be +//! associated with a domain name for use with the IPsec protocol suite. +//! +//! [RFC 4025]: https://tools.ietf.org/html/rfc4025 + +// Currently a false positive on Ipseckey. We cannot apply it there because +// the allow attribute doesn't get copied to the code generated by serde. +#![allow(clippy::needless_maybe_sized)] + +use crate::base::cmp::CanonicalOrd; +use crate::base::iana::{IpseckeyAlgorithm, IpseckeyGatewayType, Rtype}; +use crate::base::name::FlattenInto; +use crate::base::rdata::{ComposeRecordData, RecordData}; +use crate::base::scan::{Scan, Scanner, ScannerError}; +use crate::base::wire::{Composer, FormError, ParseError}; +use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; +use crate::base::{ParsedName, ToName}; +use crate::utils::base64; +use core::cmp::Ordering; +use core::{fmt, hash}; +use octseq::octets::{Octets, OctetsFrom, OctetsInto}; +use octseq::parse::Parser; + +use super::{Aaaa, A}; + +/// The IPSECKEY Resource Record is used to publish a public key that is to be +/// associated with a domain name for use with the IPsec protocol suite. +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "serde", + serde(bound( + serialize = " + N: serde::Serialize, + Octs: octseq::serde::SerializeOctets + ", + deserialize = " + N: serde::Deserialize<'de>, + Octs: octseq::serde::DeserializeOctets<'de> + ", + )) +)] +pub struct Ipseckey { + precedence: u8, + gateway_type: IpseckeyGatewayType, + algorithm: IpseckeyAlgorithm, + + /// "The gateway to which an IPsec tunnel may be created" + /// + /// There are three formats: + /// + /// - IPv4 address: This is a 32-bit number in network byte order. + /// - IPv6 address: This is a 128-bit number in network byte order. + /// - A normal wire-encoded domain name, always uncompressed. + /// - The domain MUST equal '.' if the gateway type is + /// IpseckeyGatewayType::NONE + gateway: IpseckeyGateway, + + // May be zero bytes long + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "octseq::serde::SerializeOctets::serialize_octets", + deserialize_with = "octseq::serde::DeserializeOctets::deserialize_octets", + ) + )] + key: Octs, +} + +//------------ Ipseckey ------------------------------------------------------ + +impl Ipseckey<(), ()> { + /// The rtype of this record data type. + pub(crate) const RTYPE: Rtype = Rtype::IPSECKEY; +} + +impl Ipseckey { + /// Create a Ipseckey record data from provided parameters. + pub fn new( + precedence: u8, + algorithm: IpseckeyAlgorithm, + gateway: IpseckeyGateway, + key: Octs, + ) -> Self { + let gateway_type = match gateway { + IpseckeyGateway::None => IpseckeyGatewayType::NONE, + IpseckeyGateway::Ipv4(_) => IpseckeyGatewayType::IPV4, + IpseckeyGateway::Ipv6(_) => IpseckeyGatewayType::IPV6, + IpseckeyGateway::Name(_) => IpseckeyGatewayType::NAME, + }; + Self { + precedence, + gateway_type, + algorithm, + gateway, + key, + } + } + + /// Get the precedence field. + pub fn precedence(&self) -> u8 { + self.precedence + } + + /// Get the gateway type field. + pub fn gateway_type(&self) -> IpseckeyGatewayType { + self.gateway_type + } + + /// Get the public key algorithm field. + pub fn algorithm(&self) -> IpseckeyAlgorithm { + self.algorithm + } + + /// Get the gateway field. + pub fn gateway(&self) -> &IpseckeyGateway { + &self.gateway + } + + /// Get the public key field. + pub fn key(&self) -> &Octs { + &self.key + } + + /// Parse the record data from zonefile format. + pub fn scan>( + scanner: &mut S, + ) -> Result + where + Octs: AsRef<[u8]>, + { + let precedence = u8::scan(scanner)?; + // Using u8::scan instead of Ipseckey{GatewayType,Algorithm}::scan to + // restrict the allowed input to integers and disallow mnemonics. + let gateway_type = u8::scan(scanner)?.into(); + let algorithm = u8::scan(scanner)?.into(); + let gateway = IpseckeyGateway::scan(scanner, gateway_type)?; + let key = scanner.convert_entry(base64::SymbolConverter::new())?; + if key.as_ref().is_empty() && algorithm != IpseckeyAlgorithm::NONE { + return Err(ScannerError::custom("Missing IPSECKEY public key field. The public key field may only be omitted when the algorithm is specified as 0")); + } + + Ok(Self { + precedence, + gateway_type, + algorithm, + gateway, + key, + }) + } + + pub(super) fn flatten( + self, + ) -> Result, N::AppendError> + where + TargetOcts: OctetsFrom, + N: FlattenInto, + { + let Ipseckey { + precedence, + gateway_type, + algorithm, + gateway, + key, + } = self; + + Ok(Ipseckey { + precedence, + gateway_type, + algorithm, + gateway: gateway.flatten()?, + key: key.try_octets_into()?, + }) + } + + pub(super) fn convert_octets( + self, + ) -> Result, TargetOcts::Error> + where + TargetOcts: OctetsFrom, + TargetName: OctetsFrom, + { + let Ipseckey { + precedence, + gateway_type, + algorithm, + gateway, + key, + } = self; + + Ok(Ipseckey { + precedence, + gateway_type, + algorithm, + gateway: gateway.convert_octets()?, + key: key.try_octets_into()?, + }) + } +} + +impl Ipseckey> { + /// Parse the record data from wire format. + pub fn parse<'a, Src: Octets = Octs> + ?Sized>( + parser: &mut Parser<'a, Src>, + ) -> Result { + let precedence = parser.parse_u8()?; + let gateway_type = IpseckeyGatewayType::parse(parser)?; + let algorithm = IpseckeyAlgorithm::parse(parser)?; + let gateway = IpseckeyGateway::parse(parser, gateway_type)?; + let len_key = parser.remaining(); + if len_key == 0 && algorithm != IpseckeyAlgorithm::NONE { + return Err(ParseError::ShortInput); + } + let key = parser.parse_octets(len_key)?; + Ok(Self { + precedence, + gateway_type, + algorithm, + gateway, + key, + }) + } +} + +impl RecordData for Ipseckey { + fn rtype(&self) -> Rtype { + Ipseckey::RTYPE + } +} + +impl, N: ToName> ComposeRecordData for Ipseckey { + fn rdlen(&self, _compress: bool) -> Option { + Some( + // precedence=1 + gateway_type=1 + algorithm=1 + gateway + key + u16::try_from( + 1 + 1 + + 1 + + self.gateway.rdlen() as usize + + self.key.as_ref().len(), + ) + .expect("long IPSECKEY rdata"), + ) + } + + fn compose_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + target.append_slice(&[self.precedence])?; + target.append_slice(&[self.gateway_type.into()])?; + target.append_slice(&[self.algorithm.into()])?; + self.gateway.compose_rdata(target)?; + target.append_slice(self.key.as_ref()) + } + + fn compose_canonical_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + self.compose_rdata(target) + } +} + +impl, N: hash::Hash> hash::Hash for Ipseckey { + fn hash(&self, state: &mut H) { + self.precedence.hash(state); + self.gateway_type.hash(state); + self.algorithm.hash(state); + self.gateway.hash(state); + self.key.as_ref().hash(state); + } +} + +impl PartialEq> + for Ipseckey +where + Octs: AsRef<[u8]> + ?Sized, + OtherOcts: AsRef<[u8]> + ?Sized, + N: ToName, + OtherName: ToName, +{ + fn eq(&self, other: &Ipseckey) -> bool { + self.precedence.eq(&other.precedence) + && self.gateway_type.eq(&other.gateway_type) + && self.algorithm.eq(&other.algorithm) + && self.gateway.eq(&other.gateway) + && self.key.as_ref().eq(other.key.as_ref()) + } +} + +impl + ?Sized, N: ToName> Eq for Ipseckey {} + +impl, N: fmt::Display> fmt::Display for Ipseckey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} {} {} {} ( ", + self.precedence, + u8::from(self.gateway_type), + u8::from(self.algorithm), + self.gateway, + )?; + base64::display(&self.key, f)?; + write!(f, " )") + } +} + +impl, N: fmt::Debug> fmt::Debug for Ipseckey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // f.write_str("Ipseckey(")?; + // fmt::Display::fmt(self, f)?; + // f.write_str(")") + f.debug_struct("Ipseckey") + .field("precedence", &self.precedence) + .field("gateway_type", &self.gateway_type) + .field("algorithm", &self.algorithm) + .field("gateway", &self.gateway) + .field( + "key", + &format_args!("{}", base64::encode_display(&self.key)), + ) + .finish() + } +} + +impl, N: ToName> ZonefileFmt for Ipseckey { + fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { + p.block(|p| { + p.write_token(self.precedence)?; + p.write_comment("precedence")?; + p.write_show(self.gateway_type)?; + p.write_comment("gateway type")?; + p.write_show(self.algorithm)?; + p.write_comment("algorithm")?; + p.write_show(&self.gateway)?; + p.write_comment("gateway")?; + p.write_token(base64::encode_display(&self.key)) + }) + } +} + +impl PartialOrd> + for Ipseckey +where + Octs: AsRef<[u8]>, + OtherOcts: AsRef<[u8]>, + N: ToName, + OtherName: ToName, +{ + fn partial_cmp( + &self, + other: &Ipseckey, + ) -> Option { + match self.precedence.partial_cmp(&other.precedence) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.gateway_type.partial_cmp(&other.gateway_type) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.algorithm.partial_cmp(&other.algorithm) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.gateway.partial_cmp(&other.gateway) { + Some(Ordering::Equal) => {} + other => return other, + } + self.key.as_ref().partial_cmp(other.key.as_ref()) + } +} + +impl + CanonicalOrd> for Ipseckey +where + Octs: AsRef<[u8]>, + OtherOcts: AsRef<[u8]>, + N: ToName, + OtherName: ToName, +{ + fn canonical_cmp( + &self, + other: &Ipseckey, + ) -> Ordering { + match self.precedence.cmp(&other.precedence) { + Ordering::Equal => {} + other => return other, + } + match self.gateway_type.cmp(&other.gateway_type) { + Ordering::Equal => {} + other => return other, + } + match self.algorithm.cmp(&other.algorithm) { + Ordering::Equal => {} + other => return other, + } + match self.gateway.partial_cmp(&other.gateway) { + Some(Ordering::Equal) => {} + Some(other) => return other, + None => unreachable!("The gateway will be the same variant and therefore have an ordering, because the gateway_type above was Equal"), + } + self.key.as_ref().cmp(other.key.as_ref()) + } +} + +impl, N: ToName> Ord for Ipseckey { + fn cmp(&self, other: &Self) -> Ordering { + match self.precedence.cmp(&other.precedence) { + Ordering::Equal => {} + other => return other, + } + match self.gateway_type.cmp(&other.gateway_type) { + Ordering::Equal => {} + other => return other, + } + match self.algorithm.cmp(&other.algorithm) { + Ordering::Equal => {} + other => return other, + } + match self.gateway.partial_cmp(&other.gateway) { + Some(Ordering::Equal) => {} + Some(other) => return other, + None => unreachable!("The gateway will be the same variant and therefore have an ordering, because the gateway_type above was Equal"), + } + self.key.as_ref().cmp(other.key.as_ref()) + } +} + +//------------ IpseckeyGateway ----------------------------------------------- + +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum IpseckeyGateway { + None, + Ipv4(A), + Ipv6(Aaaa), + Name(N), +} + +impl IpseckeyGateway { + pub fn rdlen(&self) -> u16 + where + N: ToName, + { + match self { + IpseckeyGateway::None => 0, + IpseckeyGateway::Ipv4(_) => 4, + IpseckeyGateway::Ipv6(_) => 16, + IpseckeyGateway::Name(n) => n.compose_len(), + } + } + + pub fn is_correct_gateway_type(&self, gwt: IpseckeyGatewayType) -> bool { + matches!( + (self, gwt), + (IpseckeyGateway::None, IpseckeyGatewayType::NONE) + | (IpseckeyGateway::Ipv4(_), IpseckeyGatewayType::IPV4) + | (IpseckeyGateway::Ipv6(_), IpseckeyGatewayType::IPV6) + | (IpseckeyGateway::Name(_), IpseckeyGatewayType::NAME) + ) + } + + pub fn scan>( + scanner: &mut S, + gateway_type: IpseckeyGatewayType, + ) -> Result { + Ok(match gateway_type { + IpseckeyGatewayType::NONE => { + scanner.scan_ascii_str(|s| { + if s == "." { + Ok(Self::None) + } else { + Err(ScannerError::custom("Invalid IPSECKEY gateway. As the gateway type is specified as 0 (None), the gateway MUST be set to '.'")) + } + })? + }, + IpseckeyGatewayType::IPV4 => Self::Ipv4(A::scan(scanner)?), + IpseckeyGatewayType::IPV6 => Self::Ipv6(Aaaa::scan(scanner)?), + IpseckeyGatewayType::NAME => Self::Name(scanner.scan_name()?), + _ => { + return Err(ScannerError::custom( + "Unknown IPSECKEY gateway type", + )) + } + }) + } + + pub(super) fn flatten( + self, + ) -> Result, N::AppendError> + where + N: FlattenInto, + { + Ok(match self { + IpseckeyGateway::None => IpseckeyGateway::None, + IpseckeyGateway::Ipv4(a) => IpseckeyGateway::Ipv4(a), + IpseckeyGateway::Ipv6(aaaa) => IpseckeyGateway::Ipv6(aaaa), + IpseckeyGateway::Name(n) => { + IpseckeyGateway::Name(n.try_flatten_into()?) + } + }) + } + + pub(super) fn convert_octets>( + self, + ) -> Result, Target::Error> { + Ok(match self { + IpseckeyGateway::None => IpseckeyGateway::None, + IpseckeyGateway::Ipv4(a) => IpseckeyGateway::Ipv4(a), + IpseckeyGateway::Ipv6(aaaa) => IpseckeyGateway::Ipv6(aaaa), + IpseckeyGateway::Name(n) => { + IpseckeyGateway::Name(n.try_octets_into()?) + } + }) + } + + fn compose_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> + where + N: ToName, + { + match self { + IpseckeyGateway::None => (), + IpseckeyGateway::Ipv4(a) => a.compose_rdata(target)?, + IpseckeyGateway::Ipv6(aaaa) => aaaa.compose_rdata(target)?, + IpseckeyGateway::Name(n) => n.compose(target)?, + }; + Ok(()) + } +} + +impl hash::Hash for IpseckeyGateway { + fn hash(&self, state: &mut H) { + match self { + IpseckeyGateway::None => todo!(), + IpseckeyGateway::Ipv4(a) => a.hash(state), + IpseckeyGateway::Ipv6(aaaa) => aaaa.hash(state), + IpseckeyGateway::Name(n) => n.hash(state), + } + } +} + +impl PartialEq> + for IpseckeyGateway +where + N: ToName, + OtherName: ToName, +{ + fn eq(&self, other: &IpseckeyGateway) -> bool { + match (self, other) { + (IpseckeyGateway::None, IpseckeyGateway::None) => true, + (IpseckeyGateway::Ipv4(a), IpseckeyGateway::Ipv4(o)) => a.eq(o), + (IpseckeyGateway::Ipv6(aaaa), IpseckeyGateway::Ipv6(o)) => { + aaaa.eq(o) + } + (IpseckeyGateway::Name(n), IpseckeyGateway::Name(o)) => { + n.name_eq(o) + } + _ => false, + } + } +} + +impl PartialOrd> + for IpseckeyGateway +where + N: ToName, + OtherName: ToName, +{ + fn partial_cmp( + &self, + other: &IpseckeyGateway, + ) -> Option { + match (self, other) { + (IpseckeyGateway::None, IpseckeyGateway::None) => { + Some(Ordering::Equal) + } + (IpseckeyGateway::Ipv4(a), IpseckeyGateway::Ipv4(o)) => { + a.partial_cmp(o) + } + (IpseckeyGateway::Ipv6(aaaa), IpseckeyGateway::Ipv6(o)) => { + aaaa.partial_cmp(o) + } + (IpseckeyGateway::Name(n), IpseckeyGateway::Name(o)) => { + Some(n.name_cmp(o)) + } + _ => None, + } + } +} + +impl IpseckeyGateway> { + pub fn parse<'a, Src: Octets = Octs> + ?Sized>( + parser: &mut Parser<'a, Src>, + gateway_type: IpseckeyGatewayType, + ) -> Result { + let len_gateway = match gateway_type { + IpseckeyGatewayType::NONE => Some(0), + IpseckeyGatewayType::IPV4 => Some(4), + IpseckeyGatewayType::IPV6 => Some(16), + IpseckeyGatewayType::NAME => None, + _ => { + return Err(ParseError::Form(FormError::new( + "Unknown IPSECKEY gateway type", + ))) + } + }; + let remaining = parser.remaining(); + let gateway = if let Some(len_gateway) = len_gateway { + if remaining < len_gateway { + return Err(ParseError::ShortInput); + } + match gateway_type { + IpseckeyGatewayType::NONE => IpseckeyGateway::None, + IpseckeyGatewayType::IPV4 => { + IpseckeyGateway::Ipv4(A::parse(parser)?) + } + IpseckeyGatewayType::IPV6 => { + IpseckeyGateway::Ipv6(Aaaa::parse(parser)?) + } + _ => unreachable!(), + } + } else { + // Minimal length unknown, it contains a domain name + let name = ParsedName::parse(parser)?; + if name.is_compressed() { + return Err(ParseError::Form(FormError::new( + "IPSECKEY gateway contains compressed name", + ))); + } + IpseckeyGateway::Name(name) + }; + Ok(gateway) + } +} + +impl ZonefileFmt for IpseckeyGateway { + fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { + match self { + IpseckeyGateway::None => (), + IpseckeyGateway::Ipv4(a) => p.write_show(a)?, + IpseckeyGateway::Ipv6(aaaa) => p.write_show(aaaa)?, + IpseckeyGateway::Name(n) => p.write_token(n.fmt_with_dot())?, + }; + Ok(()) + } +} + +impl fmt::Display for IpseckeyGateway { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IpseckeyGateway::None => write!(f, "."), + IpseckeyGateway::Ipv4(a) => write!(f, "{a}"), + IpseckeyGateway::Ipv6(aaaa) => write!(f, "{aaaa}"), + IpseckeyGateway::Name(n) => write!(f, "{n}"), + } + } +} + +impl fmt::Debug for IpseckeyGateway { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IpseckeyGateway::None => write!(f, "IpseckeyGateway::None"), + IpseckeyGateway::Ipv4(a) => { + write!(f, "IpseckeyGateway::Ipv4({a:?})") + } + IpseckeyGateway::Ipv6(aaaa) => { + write!(f, "IpseckeyGateway::Ipv6({aaaa:?})") + } + IpseckeyGateway::Name(n) => { + write!(f, "IpseckeyGateway::Name({n:?})") + } + } + } +} + +#[cfg(test)] +#[cfg(all(feature = "std", feature = "bytes"))] +mod test { + use super::*; + use crate::base::rdata::test::{ + test_compose_parse, test_rdlen, test_scan, + }; + use crate::base::Name; + use crate::utils::base64::decode; + use core::str::FromStr; + use std::net::{Ipv4Addr, Ipv6Addr}; + use std::string::ToString; + use std::vec::Vec; + + #[test] + // allow redundant_closure because of lifetime shenanigans + // in test_compose_parse(...::parse), "FnOnce is not general enough" + #[allow(clippy::redundant_closure)] + fn ipseckey_compose_parse_scan() { + // From https://www.rfc-editor.org/rfc/rfc4025.html#section-3.2 + // IPSECKEY ( 10 1 2 192.0.2.38 AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + // IPSECKEY ( 10 0 2 . AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + // IPSECKEY ( 10 1 2 192.0.2.3 AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + // IPSECKEY ( 10 3 2 mygateway.example.com. AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + // IPSECKEY ( 10 2 2 2001:0DB8:0:8002::2000:1 AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + + let key_str = "AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ=="; + let key: Vec = decode(key_str).unwrap(); + for (precedence, gateway_type, algorithm, gateway_str, gateway) in [ + ( + 10, + IpseckeyGatewayType::from_int(1), + 2.into(), + "192.0.2.38", + IpseckeyGateway::>>::Ipv4( + Ipv4Addr::new(192, 0, 2, 38).into(), + ), + ), + ( + 10, + 0.into(), + 2.into(), + ".", + IpseckeyGateway::>>::None, + ), + ( + 10, + 1.into(), + 2.into(), + "192.0.2.3", + IpseckeyGateway::>>::Ipv4( + Ipv4Addr::new(192, 0, 2, 3).into(), + ), + ), + ( + 10, + 3.into(), + 2.into(), + "mygateway.example.com.", + IpseckeyGateway::>>::Name( + Name::from_str("mygateway.example.com.").unwrap(), + ), + ), + ( + 10, + 2.into(), + 2.into(), + "2001:0DB8:0:8002::2000:1", + IpseckeyGateway::>>::Ipv6( + Ipv6Addr::new( + 0x2001, 0x0DB8, 0x0, 0x8002, 0x0, 0x0, 0x2000, 0x1, + ) + .into(), + ), + ), + ] { + let rdata = Ipseckey::new(precedence, algorithm, gateway, &key); + test_rdlen(&rdata); + test_compose_parse(&rdata, |parser| Ipseckey::parse(parser)); + test_scan( + &[ + &precedence.to_string(), + &u8::from(gateway_type).to_string(), + &u8::from(algorithm).to_string(), + gateway_str, + key_str, + ], + Ipseckey::scan, + &rdata, + ); + } + + // IPSECKEY ( 10 0 0 . ) + let rdata = Ipseckey::new( + 10, + 0.into(), + IpseckeyGateway::>>::None, + &[], + ); + test_rdlen(&rdata); + test_compose_parse(&rdata, |parser| Ipseckey::parse(parser)); + test_scan( + &[&10.to_string(), &0.to_string(), &0.to_string(), "."], + Ipseckey::scan, + &rdata, + ); + } + + #[test] + #[should_panic] + // allow redundant_closure because of lifetime shenanigans + // in test_compose_parse(...::parse), "FnOnce is not general enough" + #[allow(clippy::redundant_closure)] + fn ipseckey_scan_wrong_gateway() { + // IPSECKEY ( 10 0 2 this.should.be.just.dot. AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + let precedence = 10; + let gateway_type = IpseckeyGatewayType::from_int(0); + let algorithm = 2.into(); + let wrong_gateway_str = "this.should.be.just.dot."; + // let wrong_gateway: IpseckeyGateway>> = + // IpseckeyGateway::Name(Name::from_str(wrong_gateway_str).unwrap()); + let correct_gateway = IpseckeyGateway::>>::None; + let key_str = "AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ=="; + let key: Vec = decode(key_str).unwrap(); + let correct_rdata = + Ipseckey::new(precedence, algorithm, correct_gateway, key); + // This should panic in the unwrap within test_scan + test_scan( + &[ + &precedence.to_string(), + &u8::from(gateway_type).to_string(), + &u8::from(algorithm).to_string(), + wrong_gateway_str, + key_str, + ], + Ipseckey::scan, + &correct_rdata, + ); + } + + #[cfg(feature = "zonefile")] + #[test] + fn ipseckey_parse_zonefile() { + use crate::rdata::ZoneRecordData; + use crate::zonefile::inplace::{Entry, Zonefile}; + + // From https://www.rfc-editor.org/rfc/rfc4025.html#section-3.2 + let content = r#" +arpa. 86400 IN SOA ns1 admin 2018031900 ( + 1800 900 604800 86400 ) + 86400 IN NS ns1 + 86400 IN NS ns2 +ns1 3600 IN A 203.0.113.63 +ns2 3600 IN AAAA 2001:db8::63 + +38.2.0.192.in-addr.arpa. 7200 IN IPSECKEY ( 10 1 2 192.0.2.38 + AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) +38.2.0.192.in-addr.arpa. 7200 IN IPSECKEY ( 10 0 2 . + AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + +38.2.0.192.in-addr.arpa. 7200 IN IPSECKEY ( 10 1 2 + 192.0.2.3 + AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + +38.1.0.192.in-addr.arpa. 7200 IN IPSECKEY ( 10 3 2 + mygateway.example.com. + AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + +$ORIGIN 1.0.0.0.0.0.2.8.B.D.0.1.0.0.2.ip6.arpa. +0.d.4.0.3.0.e.f.f.f.3.f.0.1.2.0 7200 IN IPSECKEY ( 10 2 2 + 2001:0DB8:0:8002::2000:1 + AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) +"#; + + let mut zone = Zonefile::load(&mut content.as_bytes()).unwrap(); + zone.set_origin(Name::root()); + let key_str = "AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ=="; + let key: Vec = decode(key_str).unwrap(); + let expected_ipseckeys = [ + Ipseckey::new( + 10, + 2.into(), + IpseckeyGateway::>>::Ipv4( + Ipv4Addr::new(192, 0, 2, 38).into(), + ), + &key, + ), + Ipseckey::new( + 10, + 2.into(), + IpseckeyGateway::>>::None, + &key, + ), + Ipseckey::new( + 10, + 2.into(), + IpseckeyGateway::>>::Ipv4( + Ipv4Addr::new(192, 0, 2, 3).into(), + ), + &key, + ), + Ipseckey::new( + 10, + 2.into(), + IpseckeyGateway::>>::Name( + Name::from_str("mygateway.example.com.").unwrap(), + ), + &key, + ), + Ipseckey::new( + 10, + 2.into(), + IpseckeyGateway::>>::Ipv6( + Ipv6Addr::new( + 0x2001, 0x0DB8, 0x0, 0x8002, 0x0, 0x0, 0x2000, 0x1, + ) + .into(), + ), + &key, + ), + ]; + let mut expected_idx = 0; + while let Some(entry) = zone.next_entry().unwrap() { + match entry { + Entry::Record(record) => { + if record.rtype() != Rtype::IPSECKEY { + continue; + } + match record.into_data() { + ZoneRecordData::Ipseckey(rd) => { + assert_eq!(expected_ipseckeys[expected_idx], rd); + expected_idx += 1; + } + _ => panic!(), + } + } + _ => panic!(), + } + } + } +} diff --git a/src/rdata/mod.rs b/src/rdata/mod.rs index 1d7de2b5..d4f21148 100644 --- a/src/rdata/mod.rs +++ b/src/rdata/mod.rs @@ -49,11 +49,15 @@ pub mod aaaa; pub mod cds; pub mod dname; pub mod dnssec; +pub mod ipseckey; pub mod naptr; pub mod nsec3; +pub mod openpgpkey; pub mod rfc1035; pub mod srv; +pub mod sshfp; pub mod svcb; +pub mod tlsa; pub mod tsig; pub mod zonemd; @@ -117,6 +121,11 @@ rdata_types! { Ds, } } + ipseckey::{ + zone { + Ipseckey, + } + } naptr::{ zone { Naptr, @@ -128,17 +137,32 @@ rdata_types! { Nsec3param, } } + openpgpkey::{ + zone { + Openpgpkey, + } + } srv::{ zone { Srv, } } + sshfp::{ + zone { + Sshfp, + } + } svcb::{ - pseudo { + zone { Svcb, Https, } } + tlsa::{ + zone { + Tlsa, + } + } tsig::{ pseudo { Tsig, diff --git a/src/rdata/openpgpkey.rs b/src/rdata/openpgpkey.rs new file mode 100644 index 00000000..a54535d6 --- /dev/null +++ b/src/rdata/openpgpkey.rs @@ -0,0 +1,264 @@ +//! OPENPGPKEY record data. +//! +//! The OPENPGPKEY Resource Record carries a single OpenPGP Transferable Public Key. +//! +//! [RFC 7929]: https://tools.ietf.org/html/rfc7929 + +// Currently a false positive on Openpgpkey. We cannot apply it there because +// the allow attribute doesn't get copied to the code generated by serde. +#![allow(clippy::needless_maybe_sized)] + +use crate::base::cmp::CanonicalOrd; +use crate::base::iana::Rtype; +use crate::base::rdata::{ComposeRecordData, RecordData}; +use crate::base::scan::Scanner; +use crate::base::wire::{Composer, ParseError}; +use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; +use crate::utils::base64; +use core::cmp::Ordering; +use core::{fmt, hash}; +use octseq::octets::{Octets, OctetsFrom, OctetsInto}; +use octseq::parse::Parser; + +/// The OPENPGPKEY Resource Record carries a single OpenPGP Transferable Public Key. +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Openpgpkey { + #[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>", + ) + ) + )] + key: Octs, +} + +impl Openpgpkey<()> { + /// The rtype of this record data type. + pub(crate) const RTYPE: Rtype = Rtype::OPENPGPKEY; +} + +impl Openpgpkey { + /// Create a Openpgpkey record data from provided parameters. + pub fn new(key: Octs) -> Self { + Self { key } + } + + /// Get the key field. + pub fn key(&self) -> &Octs { + &self.key + } + + /// Parse the record data from wire format. + pub fn parse<'a, Src: Octets = Octs> + ?Sized>( + parser: &mut Parser<'a, Src>, + ) -> Result { + let len = parser.remaining(); + let key = parser.parse_octets(len)?; + Ok(Self { key }) + } + + /// Parse the record data from zonefile format. + pub fn scan>( + scanner: &mut S, + ) -> Result { + let key = scanner.convert_entry(base64::SymbolConverter::new())?; + + Ok(Self { key }) + } + + pub(super) fn flatten>( + self, + ) -> Result, Target::Error> { + self.convert_octets() + } + + pub(super) fn convert_octets>( + self, + ) -> Result, Target::Error> { + let Openpgpkey { key } = self; + + Ok(Openpgpkey { + key: key.try_octets_into()?, + }) + } +} + +impl RecordData for Openpgpkey { + fn rtype(&self) -> Rtype { + Openpgpkey::RTYPE + } +} + +impl> ComposeRecordData for Openpgpkey { + fn rdlen(&self, _compress: bool) -> Option { + Some( + // key_len + u16::try_from(self.key.as_ref().len()) + .expect("long OPENPGPKEY rdata"), + ) + } + + fn compose_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + target.append_slice(self.key.as_ref()) + } + + fn compose_canonical_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + self.compose_rdata(target) + } +} + +impl> hash::Hash for Openpgpkey { + fn hash(&self, state: &mut H) { + self.key.as_ref().hash(state); + } +} + +impl PartialEq> for Openpgpkey +where + Octs: AsRef<[u8]> + ?Sized, + Other: AsRef<[u8]> + ?Sized, +{ + fn eq(&self, other: &Openpgpkey) -> bool { + self.key.as_ref().eq(other.key.as_ref()) + } +} + +impl + ?Sized> Eq for Openpgpkey {} + +impl> fmt::Display for Openpgpkey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "( ",)?; + base64::display(&self.key, f)?; + write!(f, " )") + } +} + +impl> fmt::Debug for Openpgpkey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Openpgpkey(")?; + fmt::Display::fmt(self, f)?; + f.write_str(")") + } +} + +impl> ZonefileFmt for Openpgpkey { + fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { + p.block(|p| p.write_token(base64::encode_display(&self.key))) + } +} + +impl PartialOrd> for Openpgpkey +where + Octs: AsRef<[u8]>, + Other: AsRef<[u8]>, +{ + fn partial_cmp(&self, other: &Openpgpkey) -> Option { + self.key.as_ref().partial_cmp(other.key.as_ref()) + } +} + +impl CanonicalOrd> for Openpgpkey +where + Octs: AsRef<[u8]>, + Other: AsRef<[u8]>, +{ + fn canonical_cmp(&self, other: &Openpgpkey) -> Ordering { + self.key.as_ref().cmp(other.key.as_ref()) + } +} + +impl> Ord for Openpgpkey { + fn cmp(&self, other: &Self) -> Ordering { + self.key.as_ref().cmp(other.key.as_ref()) + } +} + +#[cfg(test)] +#[cfg(all(feature = "std", feature = "bytes"))] +mod test { + use super::*; + use crate::base::rdata::test::{ + test_compose_parse, test_rdlen, test_scan, + }; + use crate::utils::base64::decode; + use std::vec::Vec; + + #[test] + // allow redundant_closure because because of lifetime shenanigans + // in test_compose_parse(Openpgpkey::parse), "FnOnce is not general enough" + #[allow(clippy::redundant_closure)] + fn openpgpkey_compose_parse_scan() { + let key_str = "mDMEaLmjchYJKwYBBAHaRw8BAQdAaO6PfPJsT8to5dksKP1JsCmR0DqOTmVYLOv7mFeQPC+0HVRlc3QgVXNlciA8dGVzdEBubG5ldGxhYnMubmw+iJYEExYKAD4WIQT/B5WhrMOftpJIwIkxXU3+oVEKegUCaLmjcgIbAwUJBaOagAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAxXU3+oVEKemSgAP97Zvz+PWEJC9vhlSN4gVRPR9VZYhzGwfpixgRI4sqKfwD9FxJhsj43vGEbOLdsWwf/lQvkajRov5FpofS1IFy/dgi4OARouaNyEgorBgEEAZdVAQUBAQdAUQr9riJNCFWRzQ6q70B/H/o+uwvL6nGJRhWSg1v7mRkDAQgHiH4EGBYKACYWIQT/B5WhrMOftpJIwIkxXU3+oVEKegUCaLmjcgIbDAUJBaOagAAKCRAxXU3+oVEKeuX3APkB5piWOSbOPLvtiElIVTHT6gWlu1wSpVVzZEmgtnOpiQD+Kk/IFjHpT0RbgsIvI3qhnXWwHvIw4JxHS1a/piLwkwM="; + let key: Vec = decode(key_str).unwrap(); + let rdata = Openpgpkey::new(key); + test_rdlen(&rdata); + test_compose_parse(&rdata, |parser| Openpgpkey::parse(parser)); + test_scan(&[key_str], Openpgpkey::scan, &rdata); + } + + #[cfg(feature = "zonefile")] + #[test] + fn openpgpkey_parse_zonefile() { + use crate::base::Name; + use crate::rdata::ZoneRecordData; + use crate::zonefile::inplace::{Entry, Zonefile}; + + // section A.1 + let content = r#" +example. 86400 IN SOA ns1 admin 2018031900 ( + 1800 900 604800 86400 ) + 86400 IN NS ns1 + 86400 IN NS ns2 + 86400 IN OPENPGPKEY ( + mDMEaLmjchYJKwYBBAHaRw8BAQdAaO6P + fPJsT8to5dksKP1JsCmR0DqOTmVYLOv7 + mFeQPC+0HVRlc3QgVXNlciA8dGVzdEBu + bG5ldGxhYnMubmw+iJYEExYKAD4WIQT/ + B5WhrMOftpJIwIkxXU3+oVEKegUCaLmj + cgIbAwUJBaOagAULCQgHAgYVCgkICwIE + FgIDAQIeAQIXgAAKCRAxXU3+oVEKemSg + AP97Zvz+PWEJC9vhlSN4gVRPR9VZYhzG + wfpixgRI4sqKfwD9FxJhsj43vGEbOLds + Wwf/lQvkajRov5FpofS1IFy/dgi4OARo + uaNyEgorBgEEAZdVAQUBAQdAUQr9riJN + CFWRzQ6q70B/H/o+uwvL6nGJRhWSg1v7 + mRkDAQgHiH4EGBYKACYWIQT/B5WhrMOf + tpJIwIkxXU3+oVEKegUCaLmjcgIbDAUJ + BaOagAAKCRAxXU3+oVEKeuX3APkB5piW + OSbOPLvtiElIVTHT6gWlu1wSpVVzZEmg + tnOpiQD+Kk/IFjHpT0RbgsIvI3qhnXWw + HvIw4JxHS1a/piLwkwM= ) +ns1 3600 IN A 203.0.113.63 +ns2 3600 IN AAAA 2001:db8::63 +"#; + + let mut zone = Zonefile::load(&mut content.as_bytes()).unwrap(); + zone.set_origin(Name::root()); + while let Some(entry) = zone.next_entry().unwrap() { + match entry { + Entry::Record(record) => { + if record.rtype() != Rtype::OPENPGPKEY { + continue; + } + match record.into_data() { + ZoneRecordData::Openpgpkey(_) => {} + _ => panic!(), + } + } + _ => panic!(), + } + } + } +} diff --git a/src/rdata/sshfp.rs b/src/rdata/sshfp.rs new file mode 100644 index 00000000..f30da0a7 --- /dev/null +++ b/src/rdata/sshfp.rs @@ -0,0 +1,352 @@ +//! Record data from [RFC 4255]: SSHFP records. +//! +//! This RFC defines the SSHFP record type and is updated by [RFC 6594], +//! [RFC 7479], and [RFC 8709]. +//! +//! [RFC 4255]: https://tools.ietf.org/html/rfc4255 +//! [RFC 6594]: https://tools.ietf.org/html/rfc6594 +//! [RFC 7479]: https://tools.ietf.org/html/rfc7479 +//! [RFC 8709]: https://tools.ietf.org/html/rfc8709 + +// Currently a false positive on Sshfp. We cannot apply it there because +// the allow attribute doesn't get copied to the code generated by serde. +#![allow(clippy::needless_maybe_sized)] + +use crate::base::cmp::CanonicalOrd; +use crate::base::iana::{SshfpAlgorithm, SshfpType}; +use crate::base::rdata::{ComposeRecordData, RecordData}; +use crate::base::scan::Scanner; +use crate::base::wire::{Composer, ParseError}; +use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; +use crate::base::Rtype; +use crate::utils::base16; +use core::cmp::Ordering; +use core::{fmt, hash}; +use octseq::octets::{Octets, OctetsFrom, OctetsInto}; +use octseq::parse::Parser; + +//------------ Sshfp --------------------------------------------------------- + +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Sshfp { + algorithm: SshfpAlgorithm, + fingerprint_type: SshfpType, + #[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>", + ) + ) + )] + fingerprint: Octs, +} + +impl Sshfp<()> { + /// The rtype of this record data type. + pub(crate) const RTYPE: Rtype = Rtype::SSHFP; +} + +impl Sshfp { + pub fn new( + algorithm: SshfpAlgorithm, + fingerprint_type: SshfpType, + fingerprint: Octs, + ) -> Self { + Sshfp { + algorithm, + fingerprint_type, + fingerprint, + } + } + + /// Get the algorithm field. + pub fn algorithm(&self) -> SshfpAlgorithm { + self.algorithm + } + + /// Get the fingerprint type field. + pub fn fingerprint_type(&self) -> SshfpType { + self.fingerprint_type + } + + /// Get the fingerprint field. + pub fn fingerprint(&self) -> &Octs { + &self.fingerprint + } + + /// Parse the record data from wire format. + pub fn parse<'a, Src: Octets = Octs> + ?Sized>( + parser: &mut Parser<'a, Src>, + ) -> Result { + let algorithm = SshfpAlgorithm::parse(parser)?; + let fingerprint_type = SshfpType::parse(parser)?; + let len = parser.remaining(); + let fingerprint = parser.parse_octets(len)?; + Ok(Self { + algorithm, + fingerprint_type, + fingerprint, + }) + } + + /// Parse the record data from zonefile format. + pub fn scan>( + scanner: &mut S, + ) -> Result { + let algorithm = SshfpAlgorithm::scan(scanner)?; + let fingerprint_type = SshfpType::scan(scanner)?; + let fingerprint = + scanner.convert_entry(base16::SymbolConverter::new())?; + + Ok(Self { + algorithm, + fingerprint_type, + fingerprint, + }) + } + + pub(super) fn flatten>( + self, + ) -> Result, Target::Error> { + self.convert_octets() + } + + pub(super) fn convert_octets>( + self, + ) -> Result, Target::Error> { + let Sshfp { + algorithm, + fingerprint_type, + fingerprint, + } = self; + + Ok(Sshfp { + algorithm, + fingerprint_type, + fingerprint: fingerprint.try_octets_into()?, + }) + } +} + +impl RecordData for Sshfp { + fn rtype(&self) -> Rtype { + Sshfp::RTYPE + } +} + +impl> ComposeRecordData for Sshfp { + fn rdlen(&self, _compress: bool) -> Option { + Some( + // algorithm + fingerprint_type + fingerprint_len + u16::try_from(1 + 1 + self.fingerprint.as_ref().len()) + .expect("long SSHFP rdata"), + ) + } + + fn compose_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + target.append_slice(&[self.algorithm.into()])?; + target.append_slice(&[self.fingerprint_type.into()])?; + target.append_slice(self.fingerprint.as_ref()) + } + + fn compose_canonical_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + self.compose_rdata(target) + } +} + +impl> hash::Hash for Sshfp { + fn hash(&self, state: &mut H) { + self.algorithm.hash(state); + self.fingerprint_type.hash(state); + self.fingerprint.as_ref().hash(state); + } +} + +impl PartialEq> for Sshfp +where + Octs: AsRef<[u8]> + ?Sized, + Other: AsRef<[u8]> + ?Sized, +{ + fn eq(&self, other: &Sshfp) -> bool { + self.algorithm.eq(&other.algorithm) + && self.fingerprint_type.eq(&other.fingerprint_type) + && self.fingerprint.as_ref().eq(other.fingerprint.as_ref()) + } +} + +impl + ?Sized> Eq for Sshfp {} + +impl> fmt::Display for Sshfp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} {} ( ", + u8::from(self.algorithm), + u8::from(self.fingerprint_type) + )?; + base16::display(&self.fingerprint, f)?; + write!(f, " )") + } +} + +impl> fmt::Debug for Sshfp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Sshfp(")?; + fmt::Display::fmt(self, f)?; + f.write_str(")") + } +} + +impl> ZonefileFmt for Sshfp { + fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { + p.block(|p| { + p.write_token(self.algorithm)?; + p.write_token(self.fingerprint_type)?; + p.write_token(base16::encode_display(&self.fingerprint)) + }) + } +} + +impl PartialOrd> for Sshfp +where + Octs: AsRef<[u8]>, + Other: AsRef<[u8]>, +{ + fn partial_cmp(&self, other: &Sshfp) -> Option { + match self.algorithm.partial_cmp(&other.algorithm) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.fingerprint_type.partial_cmp(&other.fingerprint_type) { + Some(Ordering::Equal) => {} + other => return other, + } + self.fingerprint + .as_ref() + .partial_cmp(other.fingerprint.as_ref()) + } +} + +impl CanonicalOrd> for Sshfp +where + Octs: AsRef<[u8]>, + Other: AsRef<[u8]>, +{ + fn canonical_cmp(&self, other: &Sshfp) -> Ordering { + match self.algorithm.cmp(&other.algorithm) { + Ordering::Equal => {} + other => return other, + } + match self.fingerprint_type.cmp(&other.fingerprint_type) { + Ordering::Equal => {} + other => return other, + } + self.fingerprint.as_ref().cmp(other.fingerprint.as_ref()) + } +} + +impl> Ord for Sshfp { + fn cmp(&self, other: &Self) -> Ordering { + match self.algorithm.cmp(&other.algorithm) { + Ordering::Equal => {} + other => return other, + } + match self.fingerprint_type.cmp(&other.fingerprint_type) { + Ordering::Equal => {} + other => return other, + } + self.fingerprint.as_ref().cmp(other.fingerprint.as_ref()) + } +} + +#[cfg(test)] +#[cfg(all(feature = "std", feature = "bytes"))] +mod test { + use super::*; + use crate::base::rdata::test::{ + test_compose_parse, test_rdlen, test_scan, + }; + use crate::utils::base16::decode; + use std::string::ToString; + use std::vec::Vec; + + #[test] + // allow redundant_closure because because of lifetime shenanigans + // in test_compose_parse(Sshfp::parse), "FnOnce is not general enough" + #[allow(clippy::redundant_closure)] + fn sshfp_compose_parse_scan() { + let algorithm = 1.into(); + let fingerprint_type = 1.into(); + let fingerprint_str = "73d3fa022a121062580431316bfe5d56653b91c2"; + let fingerprint: Vec = decode(fingerprint_str).unwrap(); + let rdata = Sshfp::new(algorithm, fingerprint_type, fingerprint); + test_rdlen(&rdata); + test_compose_parse(&rdata, |parser| Sshfp::parse(parser)); + test_scan( + &[ + &u8::from(algorithm).to_string(), + &u8::from(fingerprint_type).to_string(), + fingerprint_str, + ], + Sshfp::scan, + &rdata, + ); + } + + #[cfg(feature = "zonefile")] + #[test] + fn sshfp_parse_zonefile() { + use crate::base::iana::{SshfpAlgorithm, SshfpType}; + use crate::base::Name; + use crate::rdata::ZoneRecordData; + use crate::zonefile::inplace::{Entry, Zonefile}; + + // section A.1 + let content = r#" +example. 86400 IN SOA ns1 admin 2018031900 ( + 1800 900 604800 86400 ) + 86400 IN NS ns1 + 86400 IN NS ns2 + 86400 IN SSHFP 1 1 ( + 73d3fa022a121062 + 580431316bfe5d56 + 653b91c2 ) +ns1 3600 IN A 203.0.113.63 +ns2 3600 IN AAAA 2001:db8::63 +"#; + + let mut zone = Zonefile::load(&mut content.as_bytes()).unwrap(); + zone.set_origin(Name::root()); + while let Some(entry) = zone.next_entry().unwrap() { + match entry { + Entry::Record(record) => { + if record.rtype() != Rtype::SSHFP { + continue; + } + match record.into_data() { + ZoneRecordData::Sshfp(rd) => { + assert_eq!(SshfpAlgorithm::RSA, rd.algorithm()); + assert_eq!( + SshfpType::SHA1, + rd.fingerprint_type() + ); + } + _ => panic!(), + } + } + _ => panic!(), + } + } + } +} diff --git a/src/rdata/svcb/mod.rs b/src/rdata/svcb/mod.rs index c0afb859..83142d1e 100644 --- a/src/rdata/svcb/mod.rs +++ b/src/rdata/svcb/mod.rs @@ -29,8 +29,8 @@ //! pub use self::params::{ ComposeSvcParamValue, LongSvcParam, ParseSvcParamValue, PushError, - SvcParamValue, SvcParams, SvcParamsBuilder, SvcParamsError, - UnknownSvcParam, ValueIter, + ScanSvcParamValue, SvcParamValue, SvcParams, SvcParamsBuilder, + SvcParamsError, UnknownSvcParam, ValueIter, }; pub use self::rdata::{Https, HttpsVariant, Svcb, SvcbRdata, SvcbVariant}; diff --git a/src/rdata/svcb/params.rs b/src/rdata/svcb/params.rs index d4f1a6eb..0c3b59e7 100644 --- a/src/rdata/svcb/params.rs +++ b/src/rdata/svcb/params.rs @@ -7,18 +7,26 @@ // the allow attribute doesn't get copied to the code generated by serde. #![allow(clippy::needless_maybe_sized)] +#[cfg(feature = "std")] +use std::collections::BTreeMap; +#[cfg(feature = "std")] +use std::string::String; + use super::value::AllValues; use crate::base::cmp::CanonicalOrd; use crate::base::iana::SvcParamKey; -use crate::base::scan::Symbol; +use crate::base::scan::{Scanner, ScannerError, Symbol}; use crate::base::wire::{Compose, Parse, ParseError}; use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; use core::cmp::Ordering; use core::marker::PhantomData; +#[cfg(feature = "std")] +use core::str::FromStr; use core::{cmp, fmt, hash, mem}; use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, ShortBuf}; use octseq::octets::{Octets, OctetsFrom, OctetsInto}; use octseq::parse::{Parser, ShortInput}; +use octseq::FreezeBuilder; //------------ SvcParams ----------------------------------------------------- @@ -186,6 +194,133 @@ impl> SvcParams { } } +#[cfg(feature = "std")] +impl> SvcParams { + pub fn scan>( + scanner: &mut S, + ) -> Result { + // SvcParams in presentation format MAY appear in any order, but keys + // MUST NOT be repeated. + + // SvcParam = SvcParamKey ["=" SvcParamValue] + // SvcParamValue = char-string ; See Appendix A. + // value = *OCTET ; Value before key-specific parsing + + // alpha-lc = %x61-7A ; a-z + // SvcParamKey = 1*63(alpha-lc / DIGIT / "-") + fn allowed_key_charset(ch: u8) -> bool { + (0x61..0x7A).contains(&ch) + || (0x30..0x39).contains(&ch) + || 0x2D == ch + } + + let mut builder = scanner.octets_builder()?; + + // Loop over tokens. SvcbParams might be split across multiple tokens + // if the SvcParamValue is quoted, therefore using custom + // scan_svcb_octets scanner function. + let mut key_map = BTreeMap::>::new(); + while scanner.continues() { + let mut is_key = true; + let mut key_end = None; + let mut value_start = None; + let octs = scanner.scan_svcb_octets()?; + if octs.as_ref().is_empty() { + // If someone provides an empty params token, e.g.: SVCB 10 . "" + return Err(S::Error::custom("SvcParams cannot be empty")); + } + + for (i, &ch) in octs.as_ref().iter().enumerate() { + if is_key { + if !allowed_key_charset(ch) { + if ch == b'=' { + key_end = Some(i); + is_key = false; + } else { + return Err(ScannerError::custom( + "invalid SvcParamKey", + )); + } + } + } else if value_start.is_none() { + value_start = Some(i); + } + } + + let param_key = if let Some(key_end) = key_end { + SvcParamKey::from_str(&String::from_utf8_lossy( + &octs.as_ref()[0..key_end], + )) + } else { + SvcParamKey::from_str(&String::from_utf8_lossy(octs.as_ref())) + } + .map_err(|_| ScannerError::custom("unknown SvcParamKey"))?; + + let param_value = if let Some(value_start) = value_start { + let value = &octs.as_ref()[value_start..]; + AllValues::::value_from_scan_octets( + scanner, param_key, value, + )? + .ok_or(S::Error::custom("could not parse SvcParamValue"))? + } else { + AllValues::::value_from_scan_octets( + scanner, + param_key, + &[], + )? + .ok_or(S::Error::custom( + "could not parse SvcParamValue from empty value", + ))? + }; + + if key_map.insert(param_key, param_value).is_some() { + return Err(S::Error::custom("duplicate SvcParamKey")); + } + } + + for (param_key, param_value) in &key_map { + // https://www.rfc-editor.org/rfc/rfc9460.html#name-rdata-wire-format + // When the list of SvcParams is non-empty, it contains a series + // of SvcParamKey=SvcParamValue pairs, represented as: + // - a 2-octet field containing the SvcParamKey as an integer in + // network byte order. + param_key + .compose(&mut builder) + .map_err(|_| S::Error::short_buf())?; + + // - a 2-octet field containing the length of the SvcParamValue as + // an integer between 0 and 65535 in network byte order. + let value_len = param_value.compose_len(); + value_len + .compose(&mut builder) + .map_err(|_| S::Error::short_buf())?; + + // - an octet string of this length whose contents are the + // SvcParamValue in a format determined by the SvcParamKey. + param_value + .compose_value(&mut builder) + .map_err(|_| S::Error::short_buf())?; + + // TODO: [...] Other automatically mandatory keys SHOULD NOT + // appear in the list either. (Including them wastes space and + // otherwise has no effect.) This is only relevant when parsing + // as an HTTPS RR. + if let AllValues::Mandatory(m) = ¶m_value { + for req in m.iter() { + if !key_map.contains_key(&req) { + return Err(S::Error::custom("all SvcParamKeys listed in mandatory MUST appear in SvcParams")); + } + } + } + } + + SvcParams::from_octets(builder.freeze()).map_err(|e| { + println!("error in params::from_octets = {}", &e.0); + S::Error::custom("invalid SvcParams") + }) + } +} + impl SvcParams { /// Returns a reference to the underlying octets sequence. pub fn as_octets(&self) -> &Octs { @@ -334,7 +469,7 @@ where //--- Display and Debug -impl fmt::Display for SvcParams { +impl + ?Sized> fmt::Display for SvcParams { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut parser = Parser::from_ref(self.as_slice()); let mut first = true; @@ -361,7 +496,7 @@ impl fmt::Display for SvcParams { } } -impl fmt::Debug for SvcParams { +impl + ?Sized> fmt::Debug for SvcParams { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("SvcParams") .field(&format_args!("{}", self)) @@ -371,7 +506,7 @@ impl fmt::Debug for SvcParams { //--- ZonefileFmt -impl ZonefileFmt for SvcParams { +impl + ?Sized> ZonefileFmt for SvcParams { fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { p.block(|p| { let mut parser = Parser::from_ref(self.as_slice()); @@ -476,6 +611,23 @@ pub trait ParseSvcParamValue<'a, Octs: ?Sized>: ) -> Result, ParseError>; } +/// A service binding parameter value that can be parse from wire format. +pub trait ScanSvcParamValue< + SrcOcts: AsRef<[u8]> + ?Sized, + Octs: AsRef<[u8]> + ?Sized, +>: SvcParamValue + Sized +{ + /// Scan a parameter value from octets from presentation format. + /// + /// The method should return `Ok(None)` if the type cannot parse values + /// with `key`. It should return an error if parsing fails. + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error>; +} + /// A service binding parameter value that can be composed into wire format. /// /// All value types need to be able to calculate the length of their @@ -658,6 +810,25 @@ impl<'a, Octs: Octets + ?Sized> ParseSvcParamValue<'a, Octs> } } +impl ScanSvcParamValue for UnknownSvcParam +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + let mut tmp = scanner.octets_builder()?; + tmp.append_slice(octs.as_ref()) + .map_err(|_| S::Error::short_buf())?; + Self::new(key, tmp.freeze()).map(Some).map_err(|_| { + S::Error::custom("SvcParamValue for unknown param too long") + }) + } +} + impl> ComposeSvcParamValue for UnknownSvcParam { fn compose_len(&self) -> u16 { u16::try_from(self.as_slice().len()).expect("long value") diff --git a/src/rdata/svcb/rdata.rs b/src/rdata/svcb/rdata.rs index a0d26714..b8d44cf5 100644 --- a/src/rdata/svcb/rdata.rs +++ b/src/rdata/svcb/rdata.rs @@ -9,6 +9,9 @@ use crate::base::name::{FlattenInto, ParsedName, ToName}; use crate::base::rdata::{ ComposeRecordData, LongRecordData, ParseRecordData, RecordData, }; +#[cfg(feature = "std")] +use crate::base::scan::Scan; +use crate::base::scan::{Scanner, ScannerError}; use crate::base::wire::{Compose, Composer, Parse, ParseError}; use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; use core::marker::PhantomData; @@ -179,6 +182,49 @@ impl> SvcbRdata> { } } +impl, Name: ToName> SvcbRdata { + pub fn scan>( + scanner: &mut S, + ) -> Result { + #[cfg(feature = "std")] + { + let priority = u16::scan(scanner)?; + let target = scanner.scan_name()?; + let params = SvcParams::scan(scanner)?; + + Self::new(priority, target, params) + .map_err(|_| S::Error::custom("SVCB record too long")) + } + #[cfg(not(feature = "std"))] + { + let _ = scanner; + Err(S::Error::custom("zonefile parsing of SVCB RRs is not implemented without the domain std feature")) + } + } +} + +impl, Name: ToName> SvcbRdata { + pub fn scan>( + scanner: &mut S, + ) -> Result { + #[cfg(feature = "std")] + { + let priority = u16::scan(scanner)?; + let target = scanner.scan_name()?; + // TODO: The "automatically mandatory" keys (Section 8) are "port" and "no-default-alpn". + let params = SvcParams::scan(scanner)?; + + Self::new(priority, target, params) + .map_err(|_| S::Error::custom("HTTPS record too long")) + } + #[cfg(not(feature = "std"))] + { + let _ = scanner; + Err(S::Error::custom("zonefile parsing of HTTPS RRs is not implemented without the domain std feature")) + } + } +} + impl SvcbRdata { /// Returns the priority. pub fn priority(&self) -> u16 { @@ -467,7 +513,7 @@ where impl fmt::Display for SvcbRdata where - Octs: Octets, + Octs: AsRef<[u8]>, Name: fmt::Display, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -477,7 +523,7 @@ where impl fmt::Debug for SvcbRdata where - Octs: Octets, + Octs: AsRef<[u8]>, Name: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -493,7 +539,7 @@ where impl ZonefileFmt for SvcbRdata where - Octs: Octets, + Octs: AsRef<[u8]>, Name: ToName, { fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { @@ -603,3 +649,277 @@ mod test { assert_eq!(rdata.as_ref(), buf.as_ref()); } } + +#[cfg(test)] +#[cfg(feature = "zonefile")] +mod svcb_zonefile_tests { + use super::*; + use crate::base::iana::Class; + use crate::zonefile::inplace::{self, Zonefile}; + use octseq::array::Array; + + type Octets512 = Array<512>; + + #[track_caller] + /// A helper function that takes a single resource record in zonefile + /// format as input for the Zonefile parser (because only the + /// inplace::Zonefile parser has SVCB implemented), and compares it to the + /// expected rdata octets. + fn svcb_zonefile_parse_expect( + rr: impl AsRef<[u8]>, + expected: impl AsRef<[u8]>, + is_https: bool, + ) { + let mut zonefile = Zonefile::from(rr.as_ref()); + zonefile.set_default_class(Class::IN); + let inplace::Entry::Record(scanned_rr) = + zonefile.next_entry().unwrap().unwrap() + else { + panic!() + }; + + let mut buf = Octets512::new(); + if is_https { + if let crate::rdata::ZoneRecordData::Https(scanned_rdata) = + scanned_rr.data() + { + scanned_rdata.compose_rdata(&mut buf).unwrap(); + } else { + panic!() + } + } else if let crate::rdata::ZoneRecordData::Svcb(scanned_rdata) = + scanned_rr.data() + { + scanned_rdata.compose_rdata(&mut buf).unwrap(); + } else { + panic!() + } + + assert_eq!(buf.as_ref(), expected.as_ref()); + } + + #[track_caller] + /// A helper function that takes a single resource record in zonefile + /// format as input for the Zonefile parser (because only the + /// inplace::Zonefile parser has SVCB implemented), and only unwraps the + /// result. This is used by the should panic parsing tests. + fn svcb_zonefile_parse_only( + rr: impl AsRef<[u8]>, + ) -> Result, inplace::Error> { + let mut zonefile = Zonefile::from(rr.as_ref()); + zonefile.set_default_class(Class::IN); + zonefile.next_entry() + } + + #[test] + fn test_vectors_alias_zonefile() { + // [RFC9460 Figure 2](https://www.rfc-editor.org/rfc/rfc9460.html#name-aliasmode-4) + svcb_zonefile_parse_expect( + b"example.com. HTTPS 0 foo.example.com.\n", + b"\x00\x00\x03foo\x07example\x03com\x00", + true, + ) + } + + #[test] + fn test_vectors_target_name_dot_zonefile() { + // [RFC9460 Figure 3](https://www.rfc-editor.org/rfc/rfc9460.html#name-targetname-is) + svcb_zonefile_parse_expect( + b"example.com. SVCB 1 .\n", + b"\x00\x01\x00", + false, + ) + } + + #[test] + fn test_vectors_specifies_a_port_zonefile() { + // [RFC9460 Figure 4](https://www.rfc-editor.org/rfc/rfc9460.html#name-specifies-a-port) + svcb_zonefile_parse_expect( + b"example.com. SVCB 16 foo.example.com. port=53\n", + b"\x00\x10\x03foo\x07example\x03com\x00\x00\x03\x00\x02\x00\x35", + false, + ) + } + + #[test] + fn test_vectors_generic_key_unquoted_value_zonefile() { + // [RFC9460 Figure 5](https://www.rfc-editor.org/rfc/rfc9460.html#name-a-generic-key-and-unquoted-) + svcb_zonefile_parse_expect( + b"example.com. SVCB 1 foo.example.com. key667=hello\n", + b"\x00\x01\x03foo\x07example\x03com\x00\x02\x9b\x00\x05hello", + false, + ) + } + + #[test] + fn test_vectors_generic_key_quoted_value_decimal_escape_zonefile() { + // [RFC9460 Figure 6](https://www.rfc-editor.org/rfc/rfc9460.html#name-a-generic-key-and-quoted-va) + svcb_zonefile_parse_expect( + b"example.com. SVCB 1 foo.example.com. key667=\"hello\\210qoo\"\n", + b"\x00\x01\x03foo\x07example\x03com\x00\x02\x9b\x00\x09hello\xd2qoo", + false, + ) + } + + #[test] + fn test_vectors_two_quoted_ipv6_hints_zonefile() { + // [RFC9460 Figure 7](https://www.rfc-editor.org/rfc/rfc9460.html#name-two-quoted-ipv6-hints) + svcb_zonefile_parse_expect( + b"example.com. SVCB 1 foo.example.com. ( ipv6hint=\"2001:db8::1,2001:db8::53:1\" )\n", + b"\x00\x01\ + \x03foo\x07example\x03com\x00\ + \x00\x06\ + \x00\x20\ + \x20\x01\x0d\xb8\x00\x00\x00\x00\ + \x00\x00\x00\x00\x00\x00\x00\x01\ + \x20\x01\x0d\xb8\x00\x00\x00\x00\ + \x00\x00\x00\x00\x00\x53\x00\x01", + false, + ) + } + + #[test] + fn test_vectors_ipv6_hint_embedded_ipv4_zonefile() { + // [RFC9460 Figure 8](https://www.rfc-editor.org/rfc/rfc9460.html#name-an-ipv6-hint-using-the-embe) + svcb_zonefile_parse_expect( + b"example.com. SVCB 1 example.com. ( ipv6hint=\"2001:db8:122:344::192.0.2.33\" )\n", + b"\x00\x01\ + \x07example\x03com\x00\ + \x00\x06\ + \x00\x10\ + \x20\x01\x0d\xb8\x01\x22\x03\x44\ + \x00\x00\x00\x00\xc0\x00\x02\x21", + false, + ) + } + + #[test] + fn test_vectors_ordered_in_wireformat_zonefile() { + // [RFC9460 Figure 9](https://www.rfc-editor.org/rfc/rfc9460.html#name-svcparamkey-ordering-is-arb) + svcb_zonefile_parse_expect( + b"example.com. SVCB 16 foo.example.org. ( + alpn=h2,h3-19 mandatory=ipv4hint,alpn + ipv4hint=192.0.2.1 + )\n", + b"\x00\x10\ + \x03foo\x07example\x03org\x00\ + \x00\x00\ + \x00\x04\ + \x00\x01\ + \x00\x04\ + \x00\x01\ + \x00\x09\ + \x02\ + h2\ + \x05\ + h3-19\ + \x00\x04\ + \x00\x04\ + \xc0\x00\x02\x01", + false, + ) + } + + // Disabled until SvcParamValueScanIter::next() is implemented/fixed to + // evaluate r"\\" and r"\," correctly. + // #[test] + // fn test_vectors_alpn_with_escapes_quoted_zonefile() { + // // [RFC9460 Figure 10](https://www.rfc-editor.org/rfc/rfc9460.html#name-an-alpn-value-with-an-escap) + // let rr = br#"example.com. SVCB 16 foo.example.org. alpn="f\\\\oo\\,bar,h2""#; + // let rr = [rr.as_ref(), b"\n"].concat(); + // svcb_zonefile_parse_expect( + // rr, + // b"\x00\x10\ + // \x03foo\x07example\x03org\x00\ + // \x00\x01\ + // \x00\x0c\ + // \x08\ + // f\\oo,bar\ + // \x02\ + // h2", + // false, + // ) + // } + + // Disabled until SvcParamValueScanIter::next() is implemented/fixed to + // evaluate r"\\" and r"\," correctly. + // #[test] + // fn test_vectors_alpn_with_escapes_unquoted_zonefile() { + // // [RFC9460 Figure 10](https://www.rfc-editor.org/rfc/rfc9460.html#name-an-alpn-value-with-an-escap) + // let rr = br"example.com. SVCB 16 foo.example.org. alpn=f\\\092oo\092,bar,h2"; + // let rr = [rr.as_ref(), b"\n"].concat(); + // svcb_zonefile_parse_expect( + // rr, + // b"\x00\x10\ + // \x03foo\x07example\x03org\x00\ + // \x00\x01\ + // \x00\x0c\ + // \x08\ + // f\\oo,bar\ + // \x02\ + // h2", + // false, + // ) + // } + + #[test] + fn test_vectors_multiple_instances_of_same_key_zonefile() { + // [RFC9460 Figure 11](https://www.rfc-editor.org/rfc/rfc9460.html#name-multiple-instances-of-the-s) + svcb_zonefile_parse_only( + b"example.com. SVCB 1 foo.example.com. ( + key123=abc key123=def )\n", + ) + .unwrap_err(); + } + + #[test] + fn test_vectors_missing_paramvalues_zonefile() { + // [RFC9460 Figure 12](https://www.rfc-editor.org/rfc/rfc9460.html#name-missing-svcparamvalues-that) + for rr in [ + b"example.com. SVCB 1 foo.example.com. mandatory\n".as_ref(), + b"example.com. SVCB 2 foo.example.com. alpn\n".as_ref(), + b"example.com. SVCB 3 foo.example.com. port\n".as_ref(), + b"example.com. SVCB 4 foo.example.com. ipv4hint\n".as_ref(), + b"example.com. SVCB 5 foo.example.com. ipv6hint\n".as_ref(), + ] { + svcb_zonefile_parse_only(rr).unwrap_err(); + } + } + + #[test] + fn test_vectors_empty_no_default_alpn_zonefile() { + // [RFC9460 Figure 13](https://www.rfc-editor.org/rfc/rfc9460.html#name-the-no-default-alpn-svcpara) + svcb_zonefile_parse_only( + b"example.com. SVCB 1 foo.example.com. no-default-alpn=abc\n", + ) + .unwrap_err(); + } + + #[test] + fn test_vectors_mandatory_param_missing_zonefile() { + // [RFC9460 Figure 14](https://www.rfc-editor.org/rfc/rfc9460.html#name-a-mandatory-svcparam-is-mis) + svcb_zonefile_parse_only( + b"example.com. SVCB 1 foo.example.com. mandatory=key123\n", + ) + .unwrap_err(); + } + + #[test] + fn test_vectors_mandatory_not_in_mandatory_zonefile() { + // [RFC9460 Figure 15](https://www.rfc-editor.org/rfc/rfc9460.html#name-the-mandatory-svcparamkey-m) + svcb_zonefile_parse_only( + b"example.com. SVCB 1 foo.example.com. mandatory=mandatory\n", + ) + .unwrap_err(); + } + + #[test] + fn test_vectors_multiple_of_same_key_in_mandatory_zonefile() { + // [RFC9460 Figure 16](https://www.rfc-editor.org/rfc/rfc9460.html#name-multiple-instances-of-the-sa) + svcb_zonefile_parse_only( + b"example.com. SVCB 1 foo.example.com. ( + mandatory=key123,key123 key123=abc )\n", + ) + .unwrap_err(); + } +} diff --git a/src/rdata/svcb/value.rs b/src/rdata/svcb/value.rs index 8cd7c2e1..ad81bff2 100644 --- a/src/rdata/svcb/value.rs +++ b/src/rdata/svcb/value.rs @@ -1,9 +1,18 @@ +#[cfg(feature = "std")] +use std::collections::BTreeSet; +#[cfg(feature = "std")] +use std::collections::HashSet; + use super::{ ComposeSvcParamValue, LongSvcParam, ParseSvcParamValue, PushError, - SvcParamValue, SvcParams, SvcParamsBuilder, UnknownSvcParam, + ScanSvcParamValue, SvcParamValue, SvcParams, SvcParamsBuilder, + UnknownSvcParam, }; use crate::base::iana::SvcParamKey; use crate::base::net::{Ipv4Addr, Ipv6Addr}; +use crate::base::scan::{ + ConvertSymbols, EntrySymbol, Scanner, ScannerError, Symbol, +}; use crate::base::wire::{Compose, Parse, ParseError}; use crate::utils::base64; use core::fmt::Write as _; @@ -131,6 +140,37 @@ macro_rules! values_enum { } } + #[cfg(feature = "std")] + impl ScanSvcParamValue + for AllValues + where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, + { + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + match key { + $( + $type::KEY => { + $type::value_from_scan_octets( + scanner, + key, + octs + ).map(|res| Some(Self::$type(res.unwrap()))) + } + )+ + _ => { + UnknownSvcParam::value_from_scan_octets( + scanner, key, octs + ).map(|res| res.map(Self::Unknown)) + } + } + } + } + impl> ComposeSvcParamValue for AllValues { fn compose_len(&self) -> u16 { match self { @@ -215,6 +255,8 @@ values_enum! { Ipv4Hint, Ipv6Hint, DohPath, + Ohttp, + TlsSupportedGroups, } //============ Individual Value Types ======================================== @@ -474,9 +516,61 @@ impl> Mandatory { } } +#[cfg(feature = "std")] +impl ScanSvcParamValue for Mandatory +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == Mandatory::KEY { + let mut tmp = scanner.octets_builder()?; + let mut iter = SvcParamValueScanIter::from_slice(octs.as_ref()); + // keys must be in order, therefore BTreeSet instead of HashSet + let mut keys = BTreeSet::::new(); + while let Some(item) = iter.next_no_escapes().map_err(|_| { + S::Error::custom("no escape sequences allowed in mandatory") + })? { + let k = + SvcParamKey::from_bytes(item).ok_or(S::Error::custom( + "invalid key listed in SvcParamKey mandatory", + ))?; + if k == SvcParamKey::MANDATORY { + return Err(S::Error::custom( + // https://www.rfc-editor.org/rfc/rfc9460.html#section-8-8 + "the key 'mandatory' MUST NOT appear in the mandatory keys list", + )); + } + if !keys.insert(k) { + return Err(S::Error::custom( + "mandatory contains duplicate keys", + )); + } + } + if keys.is_empty() { + return Err(S::Error::custom( + "mandatory requires at least one value", + )); + } + for k in keys { + k.compose(&mut tmp).map_err(|_| S::Error::short_buf())?; + } + Ok(Some(Self::from_octets(tmp.freeze()).map_err(|_| { + S::Error::custom("invalid svc param value for mandatory") + })?)) + } else { + Ok(None) + } + } +} + //--- Iterator -impl Iterator for MandatoryIter<'_, Octs> { +impl + ?Sized> Iterator for MandatoryIter<'_, Octs> { type Item = SvcParamKey; fn next(&mut self) -> Option { @@ -608,6 +702,55 @@ impl> Alpn { } } +impl ScanSvcParamValue for Alpn +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == Alpn::KEY { + let mut tmp = scanner.octets_builder()?; + let mut iter = SvcParamValueScanIter::from_slice(octs.as_ref()); + let mut at_least_one = false; + // For now: "Zone-file implementations MAY disallow the "," and + // "\" characters in ALPN IDs instead of implementing the + // value-list escaping procedure, relying on the opaque key format + // (e.g., key1=\002h2) in the event that these characters are + // needed." + // (https://www.rfc-editor.org/rfc/rfc9460.html#section-7.1.1-3) + // TODO: implement ALPN escaping + // while let Some(item) = iter.next() { + while let Some(item) = iter.next_no_escapes().map_err(|_| { + S::Error::custom("this implementation does not allow escape sequences in alpn") + })? { + at_least_one = true; + let len: u8 = item.len().try_into().map_err(|_| { + S::Error::custom("SvcParamValue is too long") + })?; + tmp.append_slice(&[len]) + .map_err(|_| S::Error::short_buf())?; + tmp.append_slice(item).map_err(|_| S::Error::short_buf())?; + } + + if !at_least_one { + return Err(S::Error::custom( + "expected at least one alpn-id value", + )); + } + Ok(Some(Self::from_octets(tmp.freeze()).map_err(|_| { + S::Error::custom("invalid svc param value for alpn") + })?)) + } else { + // TODO: why is it ok none if the key is wrong? (stolen from parse_value) + Ok(None) + } + } +} + //--- Iterator impl<'a, Octs: Octets + ?Sized> Iterator for AlpnIter<'a, Octs> { @@ -798,6 +941,29 @@ impl<'a, Octs: Octets + ?Sized> ParseSvcParamValue<'a, Octs> } } +impl ScanSvcParamValue for NoDefaultAlpn +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + _scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == Self::KEY { + if !octs.as_ref().is_empty() { + return Err(S::Error::custom( + "no-default-alpn takes no values", + )); + } + Ok(Some(Self)) + } else { + Ok(None) + } + } +} + impl ComposeSvcParamValue for NoDefaultAlpn { fn compose_len(&self) -> u16 { 0 @@ -890,6 +1056,35 @@ impl<'a, Octs: Octets + ?Sized> ParseSvcParamValue<'a, Octs> for Port { } } +impl ScanSvcParamValue for Port +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + _scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == Self::KEY { + if octs.as_ref().is_empty() { + return Err(S::Error::custom("port requires a value")); + } + let s = str::from_utf8(octs.as_ref()).map_err(|_| { + S::Error::custom("port value must be valid utf-8") + })?; + let port = s.parse::().map_err(|_| { + S::Error::custom( + "port value must be a 16-bit unsigned decimal number", + ) + })?; + Ok(Some(Self::new(port))) + } else { + Ok(None) + } + } +} + impl ComposeSvcParamValue for Port { fn compose_len(&self) -> u16 { u16::COMPOSE_LEN @@ -981,6 +1176,53 @@ impl> Ech { } } +impl ScanSvcParamValue for Ech +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == Ech::KEY { + if octs.as_ref().is_empty() { + return Err(S::Error::custom("ech requires as value")); + } + let mut builder = scanner.octets_builder()?; + let mut convert = base64::SymbolConverter::new(); + for ch in octs.as_ref() { + if let Some(data) = convert.process_symbol( + EntrySymbol::from(Symbol::from_octet(*ch)), + )? { + builder + .append_slice(data) + .map_err(|_| S::Error::short_buf())?; + } + } + + // if let Some(data) = convert.process_tail()? { + if let Some(data) = ::Error, + >>::process_tail(&mut convert)? + { + builder + .append_slice(data) + .map_err(|_| S::Error::short_buf())?; + } + let dec = builder.freeze(); + + Ok(Some(Self::from_octets(dec).map_err(|_| { + S::Error::custom("invalid ech param value") + })?)) + } else { + Ok(None) + } + } +} + //--- Display impl + ?Sized> fmt::Display for Ech { @@ -1099,6 +1341,47 @@ impl> Ipv4Hint { } } +impl ScanSvcParamValue for Ipv4Hint +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == Ipv4Hint::KEY { + let mut tmp = scanner.octets_builder()?; + let mut iter = SvcParamValueScanIter::from_slice(octs.as_ref()); + while let Some(item) = iter.next_no_escapes().map_err(|_| { + S::Error::custom("no escape sequences allowed in ipv4hint") + })? { + let ip = Ipv4Addr::from_str(str::from_utf8(item).map_err( + |_| S::Error::custom("invalid utf-8 in ipv4hint param"), + )?) + .map_err(|_| { + S::Error::custom("invalid ipv4 in ipv4hint param") + })?; + tmp.append_slice(&ip.octets()) + .map_err(|_| S::Error::short_buf())?; + } + // tmp.append_slice(); + let ipv4hint = Self::from_octets(tmp.freeze()).map_err(|_| { + S::Error::custom("invalid svc param value for ipv4hint") + })?; + if ipv4hint.as_slice().is_empty() { + return Err(S::Error::custom( + "ipv4hint requires at least one value", + )); + } + Ok(Some(ipv4hint)) + } else { + Ok(None) + } + } +} + impl Iterator for Ipv4HintIter<'_, Octs> { type Item = Ipv4Addr; @@ -1247,6 +1530,46 @@ impl> Ipv6Hint { } } +impl ScanSvcParamValue for Ipv6Hint +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == Ipv6Hint::KEY { + let mut tmp = scanner.octets_builder()?; + let mut iter = SvcParamValueScanIter::from_slice(octs.as_ref()); + while let Some(item) = iter.next_no_escapes().map_err(|_| { + S::Error::custom("no escape sequences allowed in ipv6hint") + })? { + let ip = Ipv6Addr::from_str(str::from_utf8(item).map_err( + |_| S::Error::custom("invalid utf-8 in ipv6hint param"), + )?) + .map_err(|_| { + S::Error::custom("invalid ipv6 in ipv6hint param") + })?; + tmp.append_slice(&ip.octets()) + .map_err(|_| S::Error::short_buf())?; + } + let ipv6hint = Self::from_octets(tmp.freeze()).map_err(|_| { + S::Error::custom("invalid svc param value for ipv6hint") + })?; + if ipv6hint.as_slice().is_empty() { + return Err(S::Error::custom( + "ipv6hint requires at least one value", + )); + } + Ok(Some(ipv6hint)) + } else { + Ok(None) + } + } +} + //--- Iterator impl Iterator for Ipv6HintIter<'_, Octs> { @@ -1368,6 +1691,32 @@ impl> DohPath { } } +impl ScanSvcParamValue for DohPath +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == DohPath::KEY { + let _ = str::from_utf8(octs.as_ref()).map_err(|_| { + S::Error::custom("dohpath must be valid UTF-8") + })?; + let mut tmp = scanner.octets_builder()?; + tmp.append_slice(octs.as_ref()) + .map_err(|_| S::Error::short_buf())?; + Ok(Some(Self::from_octets(tmp.freeze()).map_err(|_| { + S::Error::custom("invalid svc param value for dohpath") + })?)) + } else { + Ok(None) + } + } +} + //--- TryFrom and FromStr impl> TryFrom> for DohPath { @@ -1456,6 +1805,426 @@ impl + AsMut<[u8]>> SvcParamsBuilder { } } +//------------ Ohttp ------------------------------------------------- + +/// A signal that Oblivious HTTP is supported. +/// +/// The "ohttp" SvcParamKey is used to indicate that a service described in +/// a SVCB RR can be accessed as a target using an associated gateway. +/// +/// This value is always empty. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct Ohttp; + +impl Ohttp { + /// The key for this type. + const KEY: SvcParamKey = SvcParamKey::OHTTP; +} + +impl Ohttp { + /// Parses a ohttp value from its wire-format. + pub fn parse( + _parser: &mut Parser<'_, Src>, + ) -> Result { + Ok(Self) + } +} + +//--- SvcParamValue et al. + +impl SvcParamValue for Ohttp { + fn key(&self) -> SvcParamKey { + Self::KEY + } +} + +impl<'a, Octs: Octets + ?Sized> ParseSvcParamValue<'a, Octs> for Ohttp { + fn parse_value( + key: SvcParamKey, + parser: &mut Parser<'a, Octs>, + ) -> Result, ParseError> { + if key == Self::KEY { + Self::parse(parser).map(Some) + } else { + Ok(None) + } + } +} + +impl ScanSvcParamValue for Ohttp +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + _scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == Self::KEY { + if !octs.as_ref().is_empty() { + return Err(S::Error::custom("ohttp takes no values")); + } + Ok(Some(Self)) + } else { + Ok(None) + } + } +} + +impl ComposeSvcParamValue for Ohttp { + fn compose_len(&self) -> u16 { + 0 + } + + fn compose_value( + &self, + _target: &mut Target, + ) -> Result<(), Target::AppendError> { + Ok(()) + } +} + +//--- Display + +impl fmt::Display for Ohttp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("ohttp") + } +} + +//--- Extend SvcParams and SvcParamsBuilder + +impl SvcParams { + /// Returns whether the [`Ohttp`] value is present. + pub fn ohttp(&self) -> bool { + self.first::().is_some() + } +} + +impl + AsMut<[u8]>> SvcParamsBuilder { + /// Adds the [`Ohttp`] value. + pub fn ohttp(&mut self) -> Result<(), PushError> { + self.push(&Ohttp) + } +} + +//------------ TlsSupportedGroups ------------------------------------------------- + +octets_wrapper!( + /// The ‘tls-supported-groups’ service parameter value. + /// + /// This value is used to specify the endpoint's TLS supported group preferences. + /// + /// This value type is described as part of the specification for + /// TLS Key Share Prediction, currently + /// [draft-ietf-tls-key-share-prediction](https://datatracker.ietf.org/doc/draft-ietf-tls-key-share-prediction). + /// + /// A value of this type wraps an octets sequence that contains the + /// integer values of the TLS supported groups preferences in network byte + /// order. You can create a value of this type by providing an iterator + /// over the keys to be included to the [`from_keys`][Self::from_keys] + /// function. You can get an iterator over the keys in an existing value + /// through the [`iter`][Self::iter] method. + TlsSupportedGroups => TLS_SUPPORTED_GROUPS, + TlsSupportedGroupsIter +); + +impl> TlsSupportedGroups { + /// Creates a new tls-supported-groups value from an octets sequence. + /// + /// The function checks that the octets sequence contains a properly + /// encoded value of at most 65,535 octets. It does not check whether + /// there are any duplicates in the data. + pub fn from_octets(octets: Octs) -> Result { + TlsSupportedGroups::check_slice(octets.as_ref())?; + Ok(unsafe { Self::from_octets_unchecked(octets) }) + } +} + +impl TlsSupportedGroups<[u8]> { + /// Creates a new tls-supported-groups value from an octets slice. + /// + /// The function checks that the octets slice contains a properly + /// encoded value of at most 65,535 octets. It does not check whether + /// there are any duplicates in the data. + pub fn from_slice(slice: &[u8]) -> Result<&Self, ParseError> { + Self::check_slice(slice)?; + Ok(unsafe { Self::from_slice_unchecked(slice) }) + } + + /// Checks that a slice contains a properly encoded tls-supported-groups value. + fn check_slice(slice: &[u8]) -> Result<(), ParseError> { + LongSvcParam::check_len(slice.len())?; + if slice.is_empty() + || slice.len() % usize::from(u16::COMPOSE_LEN) != 0 + { + return Err(ParseError::form_error( + "invalid tls-supported-groups parameter", + )); + } + Ok(()) + } +} + +impl> TlsSupportedGroups { + /// Creates a new value from a list of keys. + /// + /// The created value will contain all the keys returned by the iterator + /// in the order provided. The function does not check for duplicates. + /// + /// Returns an error if the octets builder runs out of space or the + /// resulting value would be longer than 65,535 octets. + pub fn from_keys( + keys: impl Iterator, + ) -> Result + where + Octs: FromBuilder, + ::Builder: EmptyBuilder, + { + let mut octets = EmptyBuilder::empty(); + for item in keys { + item.compose(&mut octets)?; + } + let octets = Octs::from_builder(octets); + if LongSvcParam::check_len(octets.as_ref().len()).is_err() { + return Err(BuildValueError::LongSvcParam); + } + Ok(unsafe { Self::from_octets_unchecked(octets) }) + } +} + +impl> TlsSupportedGroups { + /// Parses a tls-supported-groups value from its wire format. + pub fn parse<'a, Src: Octets = Octs> + ?Sized>( + parser: &mut Parser<'a, Src>, + ) -> Result { + Self::from_octets(parser.parse_octets(parser.remaining())?) + } +} + +#[cfg(feature = "std")] +impl ScanSvcParamValue + for TlsSupportedGroups +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == TlsSupportedGroups::KEY { + let mut tmp = scanner.octets_builder()?; + let mut iter = SvcParamValueScanIter::from_slice(octs.as_ref()); + // keys must not be duplicated + let mut keys = HashSet::::new(); + while let Some(item) = iter.next_no_escapes().map_err(|_| { + S::Error::custom( + "no escape sequences allowed in tls-supported-groups", + ) + })? { + let k = str::from_utf8(item) + .map_err(|_| { + S::Error::custom( + "invalid key listed in SvcParamKey tls-supported-groups, must contain UTF-8 encoded numbers", + ) + })? + .parse::() + .map_err(|_| { + S::Error::custom( + "invalid key listed in SvcParamKey tls-supported-groups, must contain positive 16-bit integers", + ) + })?; + if !keys.insert(k) { + return Err(S::Error::custom( + "tls-supported-groups contains duplicate values", + )); + } + k.compose(&mut tmp).map_err(|_| S::Error::short_buf())?; + } + Ok(Some(Self::from_octets(tmp.freeze()).map_err(|_| { + S::Error::custom( + "invalid svc param value for tls-supported-groups", + ) + })?)) + } else { + Ok(None) + } + } +} + +//--- Iterator + +impl + ?Sized> Iterator + for TlsSupportedGroupsIter<'_, Octs> +{ + type Item = u16; + + fn next(&mut self) -> Option { + if self.parser.remaining() == 0 { + return None; + } + Some( + u16::parse(&mut self.parser) + .expect("invalid tls-supported-groups parameter"), + ) + } +} + +//--- Display + +impl fmt::Display for TlsSupportedGroups { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (i, v) in self.iter().enumerate() { + if i == 0 { + write!(f, "tls-supported-groups={}", v)?; + } else { + write!(f, ",{}", v)?; + } + } + Ok(()) + } +} + +//--- Extend SvcParams and SvcParamsBuilder + +impl SvcParams { + /// Returns the content of the ‘tls-supported-groups’ value if present. + pub fn tls_supported_groups( + &self, + ) -> Option>> { + self.first() + } +} + +impl + AsMut<[u8]>> SvcParamsBuilder { + /// Adds a ‘tls-supported-groups’ value with the given keys. + /// + /// Returns an error if there already is a ‘tls-supported-groups’ value, `keys` + /// contains more values than fit into a service binding parameter value, + /// or the underlying octets builder runs out of space. + pub fn tls_supported_groups( + &mut self, + keys: impl AsRef<[SvcParamKey]>, + ) -> Result<(), PushValueError> { + self.push_raw( + TlsSupportedGroups::KEY, + u16::try_from( + keys.as_ref().len() * usize::from(SvcParamKey::COMPOSE_LEN), + ) + .map_err(|_| PushValueError::LongSvcParam)?, + |octs| { + keys.as_ref().iter().try_for_each(|item| item.compose(octs)) + }, + ) + .map_err(Into::into) + } +} + +//------------ SvcParamValueScanIter ----------------------------------------- + +/// An iterator over the items of a comma separated list of octets, as used +/// in SvcParamValue [RFC9460 A.1.] with escape sequences allowed +/// +/// [RFC9460 A.1.]: https://www.rfc-editor.org/rfc/rfc9460.html#name-decoding-a-comma-separated- +struct SvcParamValueScanIter<'a> { + data: &'a [u8], + start_next: usize, + end: usize, +} + +impl<'a> SvcParamValueScanIter<'a> { + pub fn from_slice(octs: &'a [u8]) -> Self { + Self { + data: octs, + start_next: 0, + end: 0, + } + } + + // Evaluating the escape sequences for SvcParamValues was broken and is + // therefore removed for now. The needed fix is to move the escaped byte + // to the left in the buffer and overwrite the backslash that was used to + // escape said byte. So, r"\\" needs to turn into r"\" and r"\," into + // r",". + // https://www.rfc-editor.org/rfc/rfc9460.html#appendix-A.1-3 + // item = 1*OCTET + // escaped-item = 1*(item-allowed / "\," / "\\") + // comma-separated = [escaped-item *("," escaped-item)] + + // pub fn next(&mut self) -> Option<&[u8]> { + // if self.start_next >= self.data.len() { + // None + // } else { + // let mut is_escaped = false; + // let start = self.start_next; + // self.end = self.start_next; + + // loop { + // if self.end < self.data.len() { + // // Still data to read + // if !is_escaped && self.data[self.end] == b',' { + // // End of value. Return item + // break; + // } else { + // // Value not yet ended. Read more + // if is_escaped { + // // TODO: move current byte one byte to the + // // left to overwrite the escaping backslash + // is_escaped = false; + // } else if self.data[self.end] == b'\\' { + // is_escaped = true; + // } + + // self.end += 1; + // } + // } else { + // // End of data. Return item + // break; + // } + // } + + // self.start_next = self.end + 1; + // Some(&self.data[start..self.end]) + // } + // } + + /// An iterator but error on escape sequences + pub fn next_no_escapes(&mut self) -> Result, ()> { + if self.start_next >= self.data.len() { + Ok(None) + } else { + let start = self.start_next; + self.end = self.start_next; + + loop { + if self.end < self.data.len() { + // Still data to read + if self.data[self.end] == b',' { + // End of value. Return item + break; + } else { + // Value not yet ended. Read more + if self.data[self.end] == b'\\' { + // Escaping not allowed + return Err(()); + } + self.end += 1; + } + } else { + // End of data. Return item + break; + } + } + + self.start_next = self.end + 1; + Ok(Some(&self.data[start..self.end])) + } + } +} + //============ BuildValueError =============================================== //------------ BuildValueError ----------------------------------------------- diff --git a/src/rdata/tlsa.rs b/src/rdata/tlsa.rs new file mode 100644 index 00000000..56eb950c --- /dev/null +++ b/src/rdata/tlsa.rs @@ -0,0 +1,387 @@ +//! TLSA record data. +//! +//! The TLSA Resource Record is used to associate a TLS server certificate or +//! public key with the domain name of the RR +//! +//! [RFC 6698]: https://tools.ietf.org/html/rfc6698 + +// Currently a false positive on Tlsa. We cannot apply it there because +// the allow attribute doesn't get copied to the code generated by serde. +#![allow(clippy::needless_maybe_sized)] + +use crate::base::cmp::CanonicalOrd; +use crate::base::iana::{ + Rtype, TlsaCertificateUsage, TlsaMatchingType, TlsaSelector, +}; +use crate::base::rdata::{ComposeRecordData, RecordData}; +use crate::base::scan::Scanner; +use crate::base::wire::{Composer, ParseError}; +use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; +use crate::utils::base16; +use core::cmp::Ordering; +use core::{fmt, hash}; +use octseq::octets::{Octets, OctetsFrom, OctetsInto}; +use octseq::parse::Parser; + +/// The TLSA Resource Record is used to associate a TLS server certificate or +/// public key with the domain name of the RR +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Tlsa { + usage: TlsaCertificateUsage, + selector: TlsaSelector, + matching_type: TlsaMatchingType, + #[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>", + ) + ) + )] + /// Certificate Association Data + data: Octs, +} + +impl Tlsa<()> { + /// The rtype of this record data type. + pub(crate) const RTYPE: Rtype = Rtype::TLSA; +} + +impl Tlsa { + /// Create a Tlsa record data from provided parameters. + pub fn new( + usage: TlsaCertificateUsage, + selector: TlsaSelector, + matching_type: TlsaMatchingType, + data: Octs, + ) -> Self { + Self { + usage, + selector, + matching_type, + data, + } + } + + /// Get the usage field. + pub fn usage(&self) -> TlsaCertificateUsage { + self.usage + } + + /// Get the selector field. + pub fn selector(&self) -> TlsaSelector { + self.selector + } + + /// Get the hash matching_type field. + pub fn matching_type(&self) -> TlsaMatchingType { + self.matching_type + } + + /// Get the certificate association data field. + pub fn data(&self) -> &Octs { + &self.data + } + + /// Parse the record data from wire format. + pub fn parse<'a, Src: Octets = Octs> + ?Sized>( + parser: &mut Parser<'a, Src>, + ) -> Result { + let usage = TlsaCertificateUsage::parse(parser)?; + let selector = TlsaSelector::parse(parser)?; + let matching_type = TlsaMatchingType::parse(parser)?; + let len = parser.remaining(); + let data = parser.parse_octets(len)?; + Ok(Self { + usage, + selector, + matching_type, + data, + }) + } + + /// Parse the record data from zonefile format. + pub fn scan>( + scanner: &mut S, + ) -> Result { + let usage = TlsaCertificateUsage::scan(scanner)?; + let selector = TlsaSelector::scan(scanner)?; + let matching_type = TlsaMatchingType::scan(scanner)?; + let data = scanner.convert_entry(base16::SymbolConverter::new())?; + + Ok(Self { + usage, + selector, + matching_type, + data, + }) + } + + pub(super) fn flatten>( + self, + ) -> Result, Target::Error> { + self.convert_octets() + } + + pub(super) fn convert_octets>( + self, + ) -> Result, Target::Error> { + let Tlsa { + usage, + selector, + matching_type, + data, + } = self; + + Ok(Tlsa { + usage, + selector, + matching_type, + data: data.try_octets_into()?, + }) + } +} + +impl RecordData for Tlsa { + fn rtype(&self) -> Rtype { + Tlsa::RTYPE + } +} + +impl> ComposeRecordData for Tlsa { + fn rdlen(&self, _compress: bool) -> Option { + Some( + // usage + selector + matching_type + data_len + u16::try_from(1 + 1 + 1 + self.data.as_ref().len()) + .expect("long TLSA rdata"), + ) + } + + fn compose_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + target.append_slice(&[self.usage.into()])?; + target.append_slice(&[self.selector.into()])?; + target.append_slice(&[self.matching_type.into()])?; + target.append_slice(self.data.as_ref()) + } + + fn compose_canonical_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + self.compose_rdata(target) + } +} + +impl> hash::Hash for Tlsa { + fn hash(&self, state: &mut H) { + self.usage.hash(state); + self.selector.hash(state); + self.matching_type.hash(state); + self.data.as_ref().hash(state); + } +} + +impl PartialEq> for Tlsa +where + Octs: AsRef<[u8]> + ?Sized, + Other: AsRef<[u8]> + ?Sized, +{ + fn eq(&self, other: &Tlsa) -> bool { + self.usage.eq(&other.usage) + && self.selector.eq(&other.selector) + && self.matching_type.eq(&other.matching_type) + && self.data.as_ref().eq(other.data.as_ref()) + } +} + +impl + ?Sized> Eq for Tlsa {} + +impl> fmt::Display for Tlsa { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} {} {} ( ", + u8::from(self.usage), + u8::from(self.selector), + u8::from(self.matching_type) + )?; + base16::display(&self.data, f)?; + write!(f, " )") + } +} + +impl> fmt::Debug for Tlsa { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Tlsa(")?; + fmt::Display::fmt(self, f)?; + f.write_str(")") + } +} + +impl> ZonefileFmt for Tlsa { + fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { + p.block(|p| { + p.write_token(self.usage)?; + p.write_show(self.selector)?; + p.write_show(self.matching_type)?; + p.write_token(base16::encode_display(&self.data)) + }) + } +} + +impl PartialOrd> for Tlsa +where + Octs: AsRef<[u8]>, + Other: AsRef<[u8]>, +{ + fn partial_cmp(&self, other: &Tlsa) -> Option { + match self.usage.partial_cmp(&other.usage) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.selector.partial_cmp(&other.selector) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.matching_type.partial_cmp(&other.matching_type) { + Some(Ordering::Equal) => {} + other => return other, + } + self.data.as_ref().partial_cmp(other.data.as_ref()) + } +} + +impl CanonicalOrd> for Tlsa +where + Octs: AsRef<[u8]>, + Other: AsRef<[u8]>, +{ + fn canonical_cmp(&self, other: &Tlsa) -> Ordering { + match self.usage.cmp(&other.usage) { + Ordering::Equal => {} + other => return other, + } + match self.selector.cmp(&other.selector) { + Ordering::Equal => {} + other => return other, + } + match self.matching_type.cmp(&other.matching_type) { + Ordering::Equal => {} + other => return other, + } + self.data.as_ref().cmp(other.data.as_ref()) + } +} + +impl> Ord for Tlsa { + fn cmp(&self, other: &Self) -> Ordering { + match self.usage.cmp(&other.usage) { + Ordering::Equal => {} + other => return other, + } + match self.selector.cmp(&other.selector) { + Ordering::Equal => {} + other => return other, + } + match self.matching_type.cmp(&other.matching_type) { + Ordering::Equal => {} + other => return other, + } + self.data.as_ref().cmp(other.data.as_ref()) + } +} + +#[cfg(test)] +#[cfg(all(feature = "std", feature = "bytes"))] +mod test { + use super::*; + use crate::base::rdata::test::{ + test_compose_parse, test_rdlen, test_scan, + }; + use crate::utils::base16::decode; + use std::string::ToString; + use std::vec::Vec; + + #[test] + // allow redundant_closure because because of lifetime shenanigans + // in test_compose_parse(Tlsa::parse), "FnOnce is not general enough" + #[allow(clippy::redundant_closure)] + fn tlsa_compose_parse_scan() { + let usage = 0.into(); + let selector = 0.into(); + let matching_type = 1.into(); + let data_str = "d2abde240d7cd3ee6b4b28c54df034b97983a1d16e8a410e4561cb106618e971"; + let data: Vec = decode(data_str).unwrap(); + let rdata = Tlsa::new(usage, selector, matching_type, data); + test_rdlen(&rdata); + test_compose_parse(&rdata, |parser| Tlsa::parse(parser)); + test_scan( + &[ + &u8::from(usage).to_string(), + &u8::from(selector).to_string(), + &u8::from(matching_type).to_string(), + data_str, + ], + Tlsa::scan, + &rdata, + ); + } + + #[cfg(feature = "zonefile")] + #[test] + fn tlsa_parse_zonefile() { + use crate::base::iana::{ + TlsaCertificateUsage, TlsaMatchingType, TlsaSelector, + }; + use crate::base::Name; + use crate::rdata::ZoneRecordData; + use crate::zonefile::inplace::{Entry, Zonefile}; + + // section A.1 + let content = r#" +example. 86400 IN SOA ns1 admin 2018031900 ( + 1800 900 604800 86400 ) + 86400 IN NS ns1 + 86400 IN NS ns2 + 86400 IN TLSA 0 0 1 ( + d2abde240d7cd3ee6b4b28c54df034b9 + 7983a1d16e8a410e4561cb106618e971 ) +ns1 3600 IN A 203.0.113.63 +ns2 3600 IN AAAA 2001:db8::63 +"#; + + let mut zone = Zonefile::load(&mut content.as_bytes()).unwrap(); + zone.set_origin(Name::root()); + while let Some(entry) = zone.next_entry().unwrap() { + match entry { + Entry::Record(record) => { + if record.rtype() != Rtype::TLSA { + continue; + } + match record.into_data() { + ZoneRecordData::Tlsa(rd) => { + assert_eq!( + TlsaCertificateUsage::PKIX_TA, + rd.usage() + ); + assert_eq!(TlsaSelector::CERT, rd.selector()); + assert_eq!( + TlsaMatchingType::SHA2_256, + rd.matching_type() + ); + } + _ => panic!(), + } + } + _ => panic!(), + } + } + } +} diff --git a/src/zonefile/inplace.rs b/src/zonefile/inplace.rs index 2b6dbb3d..59e89212 100644 --- a/src/zonefile/inplace.rs +++ b/src/zonefile/inplace.rs @@ -664,6 +664,63 @@ impl Scanner for EntryScanner<'_> { Ok(self.zonefile.buf.split_to(write).freeze()) } + /// SVCB's SvcParams format can contain quoted SvcParamValues, therefore we need to concatenate + /// multiple tokens into a single octet sequence if they appear without whitespace, e.g.: `SVCB + /// 10 . key1="quoted value"` would normally parsed into the tokens `SVCB` `10` `.` `key1=` and + /// `quoted value`, we need the last token to be `key1=quoted value`. + fn scan_svcb_octets(&mut self) -> Result { + self.zonefile.buf.require_token()?; + + // The result will never be longer than the encoded form, so we can + // trim off everything to the left already. + self.zonefile.buf.trim_to(self.zonefile.buf.start); + + let mut write; + // Remember if we are inside a quoted value. If so the opening quote + // has already been skipped over, it is not part of the value. + let is_quoted = self.zonefile.buf.cat == ItemCat::Quoted; + + // Skip over symbols that don’t need converting at the beginning. + while self.zonefile.buf.next_ascii_symbol()?.is_some() {} + + if self.zonefile.buf.cat == ItemCat::None { + // The item has ended. Remove the double quote. + let write = if is_quoted { + self.zonefile.buf.start - 1 + } else { + self.zonefile.buf.start + }; + self.zonefile.buf.next_item()?; + return Ok(self.zonefile.buf.split_to(write).freeze()); + } + + // If we aren’t done yet, we have escaped characters to replace. + write = self.zonefile.buf.start; + + while let Some(sym) = self.zonefile.buf.next_symbol()? { + self.zonefile.buf.buf[write] = sym.into_octet()?; + write += 1; + } + + // Done. `write` marks the end. + self.zonefile.buf.next_item()?; + + // If the next token exists (i.e. is not None or LineFeed) and + // directly follows this token without whitespace in between, it is + // part of the current token/octet-string and we read further + if !self.has_space() && self.zonefile.buf.cat == ItemCat::Quoted { + while let Some(sym) = self.zonefile.buf.next_symbol()? { + self.zonefile.buf.buf[write] = sym.into_octet()?; + write += 1; + } + // Done. `write` marks the end. + self.zonefile.buf.next_item()?; + } + + let x = self.zonefile.buf.split_to(write).freeze(); + Ok(x) + } + fn scan_ascii_str(&mut self, op: F) -> Result where F: FnOnce(&str) -> Result,