Implement support for various record types (#569)

* Implement support for SSHFP records

* Implement support for OPENPGPKEY records

* Implement support for TLSA records

* Implement support for IPSECKEY records

* Add tests for IPSECKEY

* Add key length checks for IPSECKEY

* Implement support for SVCB and HTTPS records in zonefiles

* Fix reading SvcParamKey without value

* Derive ipseckey gateway type from gateway

* Add SVCB test vectors from RFC 9460

* Disable SVCB alpn escaping

Implementing evaluating the escaped backslash and comma would require
access to a mutable octet sequence, but instead of implementing that
right now, we can just forbid escapes in alpn as permitted by the RFC.
This commit is contained in:
Jannik Peters
2025-11-14 14:54:44 +01:00
committed by GitHub
parent 23cc139589
commit dfd9510729
16 changed files with 3546 additions and 13 deletions
+71
View File
@@ -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");
+6
View File
@@ -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;
+75
View File
@@ -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");
+5 -1
View File
@@ -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");
+99
View File
@@ -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");
+12
View File
@@ -211,6 +211,18 @@ pub trait Scanner {
/// It can be of any length.
fn scan_octets(&mut self) -> Result<Self::Octets, Self::Error>;
/// 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<Self::Octets, Self::Error> {
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.
+922
View File
@@ -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<Octs: ?Sized, N> {
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<N>,
// 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<Octs, N> Ipseckey<Octs, N> {
/// Create a Ipseckey record data from provided parameters.
pub fn new(
precedence: u8,
algorithm: IpseckeyAlgorithm,
gateway: IpseckeyGateway<N>,
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<N> {
&self.gateway
}
/// Get the public key field.
pub fn key(&self) -> &Octs {
&self.key
}
/// Parse the record data from zonefile format.
pub fn scan<S: Scanner<Octets = Octs, Name = N>>(
scanner: &mut S,
) -> Result<Self, S::Error>
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<TargetOcts, TargetName>(
self,
) -> Result<Ipseckey<TargetOcts, TargetName>, N::AppendError>
where
TargetOcts: OctetsFrom<Octs>,
N: FlattenInto<TargetName, AppendError = TargetOcts::Error>,
{
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<TargetOcts, TargetName>(
self,
) -> Result<Ipseckey<TargetOcts, TargetName>, TargetOcts::Error>
where
TargetOcts: OctetsFrom<Octs>,
TargetName: OctetsFrom<N, Error = TargetOcts::Error>,
{
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<Octs> Ipseckey<Octs, ParsedName<Octs>> {
/// Parse the record data from wire format.
pub fn parse<'a, Src: Octets<Range<'a> = Octs> + ?Sized>(
parser: &mut Parser<'a, Src>,
) -> Result<Self, ParseError> {
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<Octs, N> RecordData for Ipseckey<Octs, N> {
fn rtype(&self) -> Rtype {
Ipseckey::RTYPE
}
}
impl<Octs: AsRef<[u8]>, N: ToName> ComposeRecordData for Ipseckey<Octs, N> {
fn rdlen(&self, _compress: bool) -> Option<u16> {
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<Target: Composer + ?Sized>(
&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<Target: Composer + ?Sized>(
&self,
target: &mut Target,
) -> Result<(), Target::AppendError> {
self.compose_rdata(target)
}
}
impl<Octs: AsRef<[u8]>, N: hash::Hash> hash::Hash for Ipseckey<Octs, N> {
fn hash<H: hash::Hasher>(&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<Octs, N, OtherOcts, OtherName> PartialEq<Ipseckey<OtherOcts, OtherName>>
for Ipseckey<Octs, N>
where
Octs: AsRef<[u8]> + ?Sized,
OtherOcts: AsRef<[u8]> + ?Sized,
N: ToName,
OtherName: ToName,
{
fn eq(&self, other: &Ipseckey<OtherOcts, OtherName>) -> 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<Octs: AsRef<[u8]> + ?Sized, N: ToName> Eq for Ipseckey<Octs, N> {}
impl<Octs: AsRef<[u8]>, N: fmt::Display> fmt::Display for Ipseckey<Octs, N> {
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<Octs: AsRef<[u8]>, N: fmt::Debug> fmt::Debug for Ipseckey<Octs, N> {
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<Octs: AsRef<[u8]>, N: ToName> ZonefileFmt for Ipseckey<Octs, N> {
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<Octs, OtherOcts, N, OtherName> PartialOrd<Ipseckey<OtherOcts, OtherName>>
for Ipseckey<Octs, N>
where
Octs: AsRef<[u8]>,
OtherOcts: AsRef<[u8]>,
N: ToName,
OtherName: ToName,
{
fn partial_cmp(
&self,
other: &Ipseckey<OtherOcts, OtherName>,
) -> Option<Ordering> {
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<Octs, OtherOcts, N, OtherName>
CanonicalOrd<Ipseckey<OtherOcts, OtherName>> for Ipseckey<Octs, N>
where
Octs: AsRef<[u8]>,
OtherOcts: AsRef<[u8]>,
N: ToName,
OtherName: ToName,
{
fn canonical_cmp(
&self,
other: &Ipseckey<OtherOcts, OtherName>,
) -> 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<Octs: AsRef<[u8]>, N: ToName> Ord for Ipseckey<Octs, N> {
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<N> {
None,
Ipv4(A),
Ipv6(Aaaa),
Name(N),
}
impl<N> IpseckeyGateway<N> {
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<S: Scanner<Name = N>>(
scanner: &mut S,
gateway_type: IpseckeyGatewayType,
) -> Result<Self, S::Error> {
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<TargetName>(
self,
) -> Result<IpseckeyGateway<TargetName>, N::AppendError>
where
N: FlattenInto<TargetName>,
{
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<Target: OctetsFrom<N>>(
self,
) -> Result<IpseckeyGateway<Target>, 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<Target: Composer + ?Sized>(
&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<N: hash::Hash> hash::Hash for IpseckeyGateway<N> {
fn hash<H: hash::Hasher>(&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<N, OtherName> PartialEq<IpseckeyGateway<OtherName>>
for IpseckeyGateway<N>
where
N: ToName,
OtherName: ToName,
{
fn eq(&self, other: &IpseckeyGateway<OtherName>) -> 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<N, OtherName> PartialOrd<IpseckeyGateway<OtherName>>
for IpseckeyGateway<N>
where
N: ToName,
OtherName: ToName,
{
fn partial_cmp(
&self,
other: &IpseckeyGateway<OtherName>,
) -> Option<Ordering> {
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<Octs> IpseckeyGateway<ParsedName<Octs>> {
pub fn parse<'a, Src: Octets<Range<'a> = Octs> + ?Sized>(
parser: &mut Parser<'a, Src>,
gateway_type: IpseckeyGatewayType,
) -> Result<Self, ParseError> {
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<N: ToName> ZonefileFmt for IpseckeyGateway<N> {
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<N: fmt::Display> fmt::Display for IpseckeyGateway<N> {
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<N: fmt::Debug> fmt::Debug for IpseckeyGateway<N> {
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<u8> = 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::<Name<Vec<u8>>>::Ipv4(
Ipv4Addr::new(192, 0, 2, 38).into(),
),
),
(
10,
0.into(),
2.into(),
".",
IpseckeyGateway::<Name<Vec<u8>>>::None,
),
(
10,
1.into(),
2.into(),
"192.0.2.3",
IpseckeyGateway::<Name<Vec<u8>>>::Ipv4(
Ipv4Addr::new(192, 0, 2, 3).into(),
),
),
(
10,
3.into(),
2.into(),
"mygateway.example.com.",
IpseckeyGateway::<Name<Vec<u8>>>::Name(
Name::from_str("mygateway.example.com.").unwrap(),
),
),
(
10,
2.into(),
2.into(),
"2001:0DB8:0:8002::2000:1",
IpseckeyGateway::<Name<Vec<u8>>>::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::<Name<Vec<u8>>>::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<Name<Vec<u8>>> =
// IpseckeyGateway::Name(Name::from_str(wrong_gateway_str).unwrap());
let correct_gateway = IpseckeyGateway::<Name<Vec<u8>>>::None;
let key_str = "AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ==";
let key: Vec<u8> = 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<u8> = decode(key_str).unwrap();
let expected_ipseckeys = [
Ipseckey::new(
10,
2.into(),
IpseckeyGateway::<Name<Vec<u8>>>::Ipv4(
Ipv4Addr::new(192, 0, 2, 38).into(),
),
&key,
),
Ipseckey::new(
10,
2.into(),
IpseckeyGateway::<Name<Vec<u8>>>::None,
&key,
),
Ipseckey::new(
10,
2.into(),
IpseckeyGateway::<Name<Vec<u8>>>::Ipv4(
Ipv4Addr::new(192, 0, 2, 3).into(),
),
&key,
),
Ipseckey::new(
10,
2.into(),
IpseckeyGateway::<Name<Vec<u8>>>::Name(
Name::from_str("mygateway.example.com.").unwrap(),
),
&key,
),
Ipseckey::new(
10,
2.into(),
IpseckeyGateway::<Name<Vec<u8>>>::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!(),
}
}
}
}
+25 -1
View File
@@ -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<O>,
}
}
ipseckey::{
zone {
Ipseckey<O, N>,
}
}
naptr::{
zone {
Naptr<O, N>,
@@ -128,17 +137,32 @@ rdata_types! {
Nsec3param<O>,
}
}
openpgpkey::{
zone {
Openpgpkey<O>,
}
}
srv::{
zone {
Srv<N>,
}
}
sshfp::{
zone {
Sshfp<O>,
}
}
svcb::{
pseudo {
zone {
Svcb<O, N>,
Https<O, N>,
}
}
tlsa::{
zone {
Tlsa<O>,
}
}
tsig::{
pseudo {
Tsig<O, N>,
+264
View File
@@ -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<Octs: ?Sized> {
#[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<Octs> Openpgpkey<Octs> {
/// 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<Range<'a> = Octs> + ?Sized>(
parser: &mut Parser<'a, Src>,
) -> Result<Self, ParseError> {
let len = parser.remaining();
let key = parser.parse_octets(len)?;
Ok(Self { key })
}
/// Parse the record data from zonefile format.
pub fn scan<S: Scanner<Octets = Octs>>(
scanner: &mut S,
) -> Result<Self, S::Error> {
let key = scanner.convert_entry(base64::SymbolConverter::new())?;
Ok(Self { key })
}
pub(super) fn flatten<Target: OctetsFrom<Octs>>(
self,
) -> Result<Openpgpkey<Target>, Target::Error> {
self.convert_octets()
}
pub(super) fn convert_octets<Target: OctetsFrom<Octs>>(
self,
) -> Result<Openpgpkey<Target>, Target::Error> {
let Openpgpkey { key } = self;
Ok(Openpgpkey {
key: key.try_octets_into()?,
})
}
}
impl<Octs> RecordData for Openpgpkey<Octs> {
fn rtype(&self) -> Rtype {
Openpgpkey::RTYPE
}
}
impl<Octs: AsRef<[u8]>> ComposeRecordData for Openpgpkey<Octs> {
fn rdlen(&self, _compress: bool) -> Option<u16> {
Some(
// key_len
u16::try_from(self.key.as_ref().len())
.expect("long OPENPGPKEY rdata"),
)
}
fn compose_rdata<Target: Composer + ?Sized>(
&self,
target: &mut Target,
) -> Result<(), Target::AppendError> {
target.append_slice(self.key.as_ref())
}
fn compose_canonical_rdata<Target: Composer + ?Sized>(
&self,
target: &mut Target,
) -> Result<(), Target::AppendError> {
self.compose_rdata(target)
}
}
impl<Octs: AsRef<[u8]>> hash::Hash for Openpgpkey<Octs> {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
self.key.as_ref().hash(state);
}
}
impl<Octs, Other> PartialEq<Openpgpkey<Other>> for Openpgpkey<Octs>
where
Octs: AsRef<[u8]> + ?Sized,
Other: AsRef<[u8]> + ?Sized,
{
fn eq(&self, other: &Openpgpkey<Other>) -> bool {
self.key.as_ref().eq(other.key.as_ref())
}
}
impl<Octs: AsRef<[u8]> + ?Sized> Eq for Openpgpkey<Octs> {}
impl<Octs: AsRef<[u8]>> fmt::Display for Openpgpkey<Octs> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "( ",)?;
base64::display(&self.key, f)?;
write!(f, " )")
}
}
impl<Octs: AsRef<[u8]>> fmt::Debug for Openpgpkey<Octs> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Openpgpkey(")?;
fmt::Display::fmt(self, f)?;
f.write_str(")")
}
}
impl<Octs: AsRef<[u8]>> ZonefileFmt for Openpgpkey<Octs> {
fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result {
p.block(|p| p.write_token(base64::encode_display(&self.key)))
}
}
impl<Octs, Other> PartialOrd<Openpgpkey<Other>> for Openpgpkey<Octs>
where
Octs: AsRef<[u8]>,
Other: AsRef<[u8]>,
{
fn partial_cmp(&self, other: &Openpgpkey<Other>) -> Option<Ordering> {
self.key.as_ref().partial_cmp(other.key.as_ref())
}
}
impl<Octs, Other> CanonicalOrd<Openpgpkey<Other>> for Openpgpkey<Octs>
where
Octs: AsRef<[u8]>,
Other: AsRef<[u8]>,
{
fn canonical_cmp(&self, other: &Openpgpkey<Other>) -> Ordering {
self.key.as_ref().cmp(other.key.as_ref())
}
}
impl<Octs: AsRef<[u8]>> Ord for Openpgpkey<Octs> {
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<u8> = 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!(),
}
}
}
}
+352
View File
@@ -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<Octs: ?Sized> {
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<Octs> Sshfp<Octs> {
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<Range<'a> = Octs> + ?Sized>(
parser: &mut Parser<'a, Src>,
) -> Result<Self, ParseError> {
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<S: Scanner<Octets = Octs>>(
scanner: &mut S,
) -> Result<Self, S::Error> {
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<Target: OctetsFrom<Octs>>(
self,
) -> Result<Sshfp<Target>, Target::Error> {
self.convert_octets()
}
pub(super) fn convert_octets<Target: OctetsFrom<Octs>>(
self,
) -> Result<Sshfp<Target>, Target::Error> {
let Sshfp {
algorithm,
fingerprint_type,
fingerprint,
} = self;
Ok(Sshfp {
algorithm,
fingerprint_type,
fingerprint: fingerprint.try_octets_into()?,
})
}
}
impl<Octs> RecordData for Sshfp<Octs> {
fn rtype(&self) -> Rtype {
Sshfp::RTYPE
}
}
impl<Octs: AsRef<[u8]>> ComposeRecordData for Sshfp<Octs> {
fn rdlen(&self, _compress: bool) -> Option<u16> {
Some(
// algorithm + fingerprint_type + fingerprint_len
u16::try_from(1 + 1 + self.fingerprint.as_ref().len())
.expect("long SSHFP rdata"),
)
}
fn compose_rdata<Target: Composer + ?Sized>(
&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<Target: Composer + ?Sized>(
&self,
target: &mut Target,
) -> Result<(), Target::AppendError> {
self.compose_rdata(target)
}
}
impl<Octs: AsRef<[u8]>> hash::Hash for Sshfp<Octs> {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
self.algorithm.hash(state);
self.fingerprint_type.hash(state);
self.fingerprint.as_ref().hash(state);
}
}
impl<Octs, Other> PartialEq<Sshfp<Other>> for Sshfp<Octs>
where
Octs: AsRef<[u8]> + ?Sized,
Other: AsRef<[u8]> + ?Sized,
{
fn eq(&self, other: &Sshfp<Other>) -> bool {
self.algorithm.eq(&other.algorithm)
&& self.fingerprint_type.eq(&other.fingerprint_type)
&& self.fingerprint.as_ref().eq(other.fingerprint.as_ref())
}
}
impl<Octs: AsRef<[u8]> + ?Sized> Eq for Sshfp<Octs> {}
impl<Octs: AsRef<[u8]>> fmt::Display for Sshfp<Octs> {
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<Octs: AsRef<[u8]>> fmt::Debug for Sshfp<Octs> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Sshfp(")?;
fmt::Display::fmt(self, f)?;
f.write_str(")")
}
}
impl<Octs: AsRef<[u8]>> ZonefileFmt for Sshfp<Octs> {
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<Octs, Other> PartialOrd<Sshfp<Other>> for Sshfp<Octs>
where
Octs: AsRef<[u8]>,
Other: AsRef<[u8]>,
{
fn partial_cmp(&self, other: &Sshfp<Other>) -> Option<Ordering> {
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<Octs, Other> CanonicalOrd<Sshfp<Other>> for Sshfp<Octs>
where
Octs: AsRef<[u8]>,
Other: AsRef<[u8]>,
{
fn canonical_cmp(&self, other: &Sshfp<Other>) -> 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<Octs: AsRef<[u8]>> Ord for Sshfp<Octs> {
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<u8> = 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!(),
}
}
}
}
+2 -2
View File
@@ -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};
+175 -4
View File
@@ -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<Octs: AsRef<[u8]>> SvcParams<Octs> {
}
}
#[cfg(feature = "std")]
impl<Octs: AsRef<[u8]>> SvcParams<Octs> {
pub fn scan<S: Scanner<Octets = Octs>>(
scanner: &mut S,
) -> Result<Self, S::Error> {
// 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::<SvcParamKey, AllValues<Octs>>::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::<Octs>::value_from_scan_octets(
scanner, param_key, value,
)?
.ok_or(S::Error::custom("could not parse SvcParamValue"))?
} else {
AllValues::<Octs>::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) = &param_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<Octs: ?Sized> SvcParams<Octs> {
/// Returns a reference to the underlying octets sequence.
pub fn as_octets(&self) -> &Octs {
@@ -334,7 +469,7 @@ where
//--- Display and Debug
impl<Octs: Octets + ?Sized> fmt::Display for SvcParams<Octs> {
impl<Octs: AsRef<[u8]> + ?Sized> fmt::Display for SvcParams<Octs> {
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<Octs: Octets + ?Sized> fmt::Display for SvcParams<Octs> {
}
}
impl<Octs: Octets + ?Sized> fmt::Debug for SvcParams<Octs> {
impl<Octs: AsRef<[u8]> + ?Sized> fmt::Debug for SvcParams<Octs> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("SvcParams")
.field(&format_args!("{}", self))
@@ -371,7 +506,7 @@ impl<Octs: Octets + ?Sized> fmt::Debug for SvcParams<Octs> {
//--- ZonefileFmt
impl<Octs: Octets + ?Sized> ZonefileFmt for SvcParams<Octs> {
impl<Octs: AsRef<[u8]> + ?Sized> ZonefileFmt for SvcParams<Octs> {
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<Option<Self>, 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<S: Scanner<Octets = Octs>>(
scanner: &mut S,
key: SvcParamKey,
octs: &SrcOcts,
) -> Result<Option<Self>, 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<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for UnknownSvcParam<Octs>
where
Octs: AsRef<[u8]>,
SrcOcts: AsRef<[u8]> + ?Sized,
{
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
scanner: &mut S,
key: SvcParamKey,
octs: &SrcOcts,
) -> Result<Option<Self>, 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<Octs: AsRef<[u8]>> ComposeSvcParamValue for UnknownSvcParam<Octs> {
fn compose_len(&self) -> u16 {
u16::try_from(self.as_slice().len()).expect("long value")
+323 -3
View File
@@ -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<Variant, Octs: AsRef<[u8]>> SvcbRdata<Variant, Octs, ParsedName<Octs>> {
}
}
impl<Octs: AsRef<[u8]>, Name: ToName> SvcbRdata<SvcbVariant, Octs, Name> {
pub fn scan<S: Scanner<Name = Name, Octets = Octs>>(
scanner: &mut S,
) -> Result<Self, S::Error> {
#[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<Octs: AsRef<[u8]>, Name: ToName> SvcbRdata<HttpsVariant, Octs, Name> {
pub fn scan<S: Scanner<Name = Name, Octets = Octs>>(
scanner: &mut S,
) -> Result<Self, S::Error> {
#[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<Variant, Octs, Name> SvcbRdata<Variant, Octs, Name> {
/// Returns the priority.
pub fn priority(&self) -> u16 {
@@ -467,7 +513,7 @@ where
impl<Variant, Octs, Name> fmt::Display for SvcbRdata<Variant, Octs, Name>
where
Octs: Octets,
Octs: AsRef<[u8]>,
Name: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -477,7 +523,7 @@ where
impl<Variant, Octs, Name> fmt::Debug for SvcbRdata<Variant, Octs, Name>
where
Octs: Octets,
Octs: AsRef<[u8]>,
Name: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -493,7 +539,7 @@ where
impl<Variant, Octs, Name> ZonefileFmt for SvcbRdata<Variant, Octs, Name>
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<Option<inplace::Entry>, 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();
}
}
+771 -2
View File
@@ -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<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs>
for AllValues<Octs>
where
Octs: AsRef<[u8]>,
SrcOcts: AsRef<[u8]> + ?Sized,
{
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
scanner: &mut S,
key: SvcParamKey,
octs: &SrcOcts,
) -> Result<Option<Self>, 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<Octs: AsRef<[u8]>> ComposeSvcParamValue for AllValues<Octs> {
fn compose_len(&self) -> u16 {
match self {
@@ -215,6 +255,8 @@ values_enum! {
Ipv4Hint<Octs>,
Ipv6Hint<Octs>,
DohPath<Octs>,
Ohttp,
TlsSupportedGroups<Octs>,
}
//============ Individual Value Types ========================================
@@ -474,9 +516,61 @@ impl<Octs: AsRef<[u8]>> Mandatory<Octs> {
}
}
#[cfg(feature = "std")]
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for Mandatory<Octs>
where
Octs: AsRef<[u8]>,
SrcOcts: AsRef<[u8]> + ?Sized,
{
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
scanner: &mut S,
key: SvcParamKey,
octs: &SrcOcts,
) -> Result<Option<Self>, 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::<SvcParamKey>::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<Octs: Octets + ?Sized> Iterator for MandatoryIter<'_, Octs> {
impl<Octs: AsRef<[u8]> + ?Sized> Iterator for MandatoryIter<'_, Octs> {
type Item = SvcParamKey;
fn next(&mut self) -> Option<Self::Item> {
@@ -608,6 +702,55 @@ impl<Octs: AsRef<[u8]>> Alpn<Octs> {
}
}
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for Alpn<Octs>
where
Octs: AsRef<[u8]>,
SrcOcts: AsRef<[u8]> + ?Sized,
{
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
scanner: &mut S,
key: SvcParamKey,
octs: &SrcOcts,
) -> Result<Option<Self>, 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<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for NoDefaultAlpn
where
Octs: AsRef<[u8]>,
SrcOcts: AsRef<[u8]> + ?Sized,
{
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
_scanner: &mut S,
key: SvcParamKey,
octs: &SrcOcts,
) -> Result<Option<Self>, 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<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for Port
where
Octs: AsRef<[u8]>,
SrcOcts: AsRef<[u8]> + ?Sized,
{
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
_scanner: &mut S,
key: SvcParamKey,
octs: &SrcOcts,
) -> Result<Option<Self>, 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::<u16>().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<Octs: AsRef<[u8]>> Ech<Octs> {
}
}
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for Ech<Octs>
where
Octs: AsRef<[u8]>,
SrcOcts: AsRef<[u8]> + ?Sized,
{
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
scanner: &mut S,
key: SvcParamKey,
octs: &SrcOcts,
) -> Result<Option<Self>, 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) = <base64::SymbolConverter as ConvertSymbols<
EntrySymbol,
<S as Scanner>::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<Octs: AsRef<[u8]> + ?Sized> fmt::Display for Ech<Octs> {
@@ -1099,6 +1341,47 @@ impl<Octs: AsRef<[u8]>> Ipv4Hint<Octs> {
}
}
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for Ipv4Hint<Octs>
where
Octs: AsRef<[u8]>,
SrcOcts: AsRef<[u8]> + ?Sized,
{
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
scanner: &mut S,
key: SvcParamKey,
octs: &SrcOcts,
) -> Result<Option<Self>, 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<Octs: Octets + ?Sized> Iterator for Ipv4HintIter<'_, Octs> {
type Item = Ipv4Addr;
@@ -1247,6 +1530,46 @@ impl<Octs: AsRef<[u8]>> Ipv6Hint<Octs> {
}
}
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for Ipv6Hint<Octs>
where
Octs: AsRef<[u8]>,
SrcOcts: AsRef<[u8]> + ?Sized,
{
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
scanner: &mut S,
key: SvcParamKey,
octs: &SrcOcts,
) -> Result<Option<Self>, 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<Octs: Octets + ?Sized> Iterator for Ipv6HintIter<'_, Octs> {
@@ -1368,6 +1691,32 @@ impl<Octs: AsRef<[u8]>> DohPath<Octs> {
}
}
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for DohPath<Octs>
where
Octs: AsRef<[u8]>,
SrcOcts: AsRef<[u8]> + ?Sized,
{
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
scanner: &mut S,
key: SvcParamKey,
octs: &SrcOcts,
) -> Result<Option<Self>, 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<Octs: AsRef<[u8]>> TryFrom<Str<Octs>> for DohPath<Octs> {
@@ -1456,6 +1805,426 @@ impl<Octs: OctetsBuilder + AsRef<[u8]> + AsMut<[u8]>> SvcParamsBuilder<Octs> {
}
}
//------------ 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<Src: Octets + ?Sized>(
_parser: &mut Parser<'_, Src>,
) -> Result<Self, ParseError> {
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<Option<Self>, ParseError> {
if key == Self::KEY {
Self::parse(parser).map(Some)
} else {
Ok(None)
}
}
}
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for Ohttp
where
Octs: AsRef<[u8]>,
SrcOcts: AsRef<[u8]> + ?Sized,
{
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
_scanner: &mut S,
key: SvcParamKey,
octs: &SrcOcts,
) -> Result<Option<Self>, 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<Target: OctetsBuilder + ?Sized>(
&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<Octs: Octets + ?Sized> SvcParams<Octs> {
/// Returns whether the [`Ohttp`] value is present.
pub fn ohttp(&self) -> bool {
self.first::<Ohttp>().is_some()
}
}
impl<Octs: OctetsBuilder + AsRef<[u8]> + AsMut<[u8]>> SvcParamsBuilder<Octs> {
/// 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<Octs: AsRef<[u8]>> TlsSupportedGroups<Octs> {
/// 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<Self, ParseError> {
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<Octs: AsRef<[u8]>> TlsSupportedGroups<Octs> {
/// 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<Item = u16>,
) -> Result<Self, BuildValueError>
where
Octs: FromBuilder,
<Octs as 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<Octs: AsRef<[u8]>> TlsSupportedGroups<Octs> {
/// Parses a tls-supported-groups value from its wire format.
pub fn parse<'a, Src: Octets<Range<'a> = Octs> + ?Sized>(
parser: &mut Parser<'a, Src>,
) -> Result<Self, ParseError> {
Self::from_octets(parser.parse_octets(parser.remaining())?)
}
}
#[cfg(feature = "std")]
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs>
for TlsSupportedGroups<Octs>
where
Octs: AsRef<[u8]>,
SrcOcts: AsRef<[u8]> + ?Sized,
{
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
scanner: &mut S,
key: SvcParamKey,
octs: &SrcOcts,
) -> Result<Option<Self>, 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::<u16>::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::<u16>()
.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<Octs: AsRef<[u8]> + ?Sized> Iterator
for TlsSupportedGroupsIter<'_, Octs>
{
type Item = u16;
fn next(&mut self) -> Option<Self::Item> {
if self.parser.remaining() == 0 {
return None;
}
Some(
u16::parse(&mut self.parser)
.expect("invalid tls-supported-groups parameter"),
)
}
}
//--- Display
impl<Octs: Octets + ?Sized> fmt::Display for TlsSupportedGroups<Octs> {
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<Octs: Octets + ?Sized> SvcParams<Octs> {
/// Returns the content of the ‘tls-supported-groups’ value if present.
pub fn tls_supported_groups(
&self,
) -> Option<TlsSupportedGroups<Octs::Range<'_>>> {
self.first()
}
}
impl<Octs: OctetsBuilder + AsRef<[u8]> + AsMut<[u8]>> SvcParamsBuilder<Octs> {
/// 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<Option<&[u8]>, ()> {
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 -----------------------------------------------
+387
View File
@@ -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<Octs: ?Sized> {
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<Octs> Tlsa<Octs> {
/// 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<Range<'a> = Octs> + ?Sized>(
parser: &mut Parser<'a, Src>,
) -> Result<Self, ParseError> {
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<S: Scanner<Octets = Octs>>(
scanner: &mut S,
) -> Result<Self, S::Error> {
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<Target: OctetsFrom<Octs>>(
self,
) -> Result<Tlsa<Target>, Target::Error> {
self.convert_octets()
}
pub(super) fn convert_octets<Target: OctetsFrom<Octs>>(
self,
) -> Result<Tlsa<Target>, Target::Error> {
let Tlsa {
usage,
selector,
matching_type,
data,
} = self;
Ok(Tlsa {
usage,
selector,
matching_type,
data: data.try_octets_into()?,
})
}
}
impl<Octs> RecordData for Tlsa<Octs> {
fn rtype(&self) -> Rtype {
Tlsa::RTYPE
}
}
impl<Octs: AsRef<[u8]>> ComposeRecordData for Tlsa<Octs> {
fn rdlen(&self, _compress: bool) -> Option<u16> {
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<Target: Composer + ?Sized>(
&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<Target: Composer + ?Sized>(
&self,
target: &mut Target,
) -> Result<(), Target::AppendError> {
self.compose_rdata(target)
}
}
impl<Octs: AsRef<[u8]>> hash::Hash for Tlsa<Octs> {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
self.usage.hash(state);
self.selector.hash(state);
self.matching_type.hash(state);
self.data.as_ref().hash(state);
}
}
impl<Octs, Other> PartialEq<Tlsa<Other>> for Tlsa<Octs>
where
Octs: AsRef<[u8]> + ?Sized,
Other: AsRef<[u8]> + ?Sized,
{
fn eq(&self, other: &Tlsa<Other>) -> 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<Octs: AsRef<[u8]> + ?Sized> Eq for Tlsa<Octs> {}
impl<Octs: AsRef<[u8]>> fmt::Display for Tlsa<Octs> {
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<Octs: AsRef<[u8]>> fmt::Debug for Tlsa<Octs> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Tlsa(")?;
fmt::Display::fmt(self, f)?;
f.write_str(")")
}
}
impl<Octs: AsRef<[u8]>> ZonefileFmt for Tlsa<Octs> {
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<Octs, Other> PartialOrd<Tlsa<Other>> for Tlsa<Octs>
where
Octs: AsRef<[u8]>,
Other: AsRef<[u8]>,
{
fn partial_cmp(&self, other: &Tlsa<Other>) -> Option<Ordering> {
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<Octs, Other> CanonicalOrd<Tlsa<Other>> for Tlsa<Octs>
where
Octs: AsRef<[u8]>,
Other: AsRef<[u8]>,
{
fn canonical_cmp(&self, other: &Tlsa<Other>) -> 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<Octs: AsRef<[u8]>> Ord for Tlsa<Octs> {
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<u8> = 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!(),
}
}
}
}
+57
View File
@@ -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::Octets, Self::Error> {
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<F, T>(&mut self, op: F) -> Result<T, Self::Error>
where
F: FnOnce(&str) -> Result<T, Self::Error>,