From: Matt Corallo Date: Mon, 12 Feb 2024 04:21:45 +0000 (+0000) Subject: Add support for DNAME resolution X-Git-Tag: v0.5.4~54 X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=commitdiff_plain;h=8b836e295ec81c50907fe1f60ca43c82df792025;p=dnssec-prover Add support for DNAME resolution --- diff --git a/src/query.rs b/src/query.rs index bb8fd7d..a523db3 100644 --- a/src/query.rs +++ b/src/query.rs @@ -552,4 +552,31 @@ mod tests { } else { panic!(); } } } + + #[cfg(feature = "tokio")] + #[tokio::test] + async fn test_dname_wildcard_query_async() { + for resolver in ["1.1.1.1:53", "8.8.8.8:53", "9.9.9.9:53"] { + let sockaddr = resolver.to_socket_addrs().unwrap().next().unwrap(); + let query_name = "wildcard_a.wildcard_b.dname_test.dnssec_proof_tests.bitcoin.ninja.".try_into().unwrap(); + let (proof, _) = build_txt_proof_async(sockaddr, &query_name).await.unwrap(); + + let mut rrs = parse_rr_stream(&proof).unwrap(); + rrs.shuffle(&mut rand::rngs::OsRng); + let verified_rrs = verify_rr_stream(&rrs).unwrap(); + assert_eq!(verified_rrs.verified_rrs.len(), 3); + + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); + assert!(verified_rrs.valid_from < now); + assert!(verified_rrs.expires > now); + + let resolved_rrs = verified_rrs.resolve_name(&query_name); + assert_eq!(resolved_rrs.len(), 1); + if let RR::Txt(txt) = &resolved_rrs[0] { + assert_eq!(txt.name.as_str(), "cname.wildcard_test.dnssec_proof_tests.bitcoin.ninja."); + assert_eq!(txt.data, b"wildcard_test"); + } else { panic!(); } + } + } + } diff --git a/src/rr.rs b/src/rr.rs index 4761ddd..c3367a6 100644 --- a/src/rr.rs +++ b/src/rr.rs @@ -72,6 +72,8 @@ pub enum RR { TLSA(TLSA), /// A Canonical Name record CName(CName), + /// A Delegation Name record + DName(DName), /// A DNS (Public) Key resource record DnsKey(DnsKey), /// A Delegated Signer resource record @@ -88,6 +90,7 @@ impl RR { RR::NS(rr) => &rr.name, RR::Txt(rr) => &rr.name, RR::CName(rr) => &rr.name, + RR::DName(rr) => &rr.name, RR::TLSA(rr) => &rr.name, RR::DnsKey(rr) => &rr.name, RR::DS(rr) => &rr.name, @@ -102,6 +105,7 @@ impl RR { RR::NS(rr) => StaticRecord::json(rr), RR::Txt(rr) => StaticRecord::json(rr), RR::CName(rr) => StaticRecord::json(rr), + RR::DName(rr) => StaticRecord::json(rr), RR::TLSA(rr) => StaticRecord::json(rr), RR::DnsKey(rr) => StaticRecord::json(rr), RR::DS(rr) => StaticRecord::json(rr), @@ -115,6 +119,7 @@ impl RR { RR::NS(_) => NS::TYPE, RR::Txt(_) => Txt::TYPE, RR::CName(_) => CName::TYPE, + RR::DName(_) => DName::TYPE, RR::TLSA(_) => TLSA::TYPE, RR::DnsKey(_) => DnsKey::TYPE, RR::DS(_) => DS::TYPE, @@ -128,6 +133,7 @@ impl RR { RR::NS(rr) => StaticRecord::write_u16_len_prefixed_data(rr, out), RR::Txt(rr) => StaticRecord::write_u16_len_prefixed_data(rr, out), RR::CName(rr) => StaticRecord::write_u16_len_prefixed_data(rr, out), + RR::DName(rr) => StaticRecord::write_u16_len_prefixed_data(rr, out), RR::TLSA(rr) => StaticRecord::write_u16_len_prefixed_data(rr, out), RR::DnsKey(rr) => StaticRecord::write_u16_len_prefixed_data(rr, out), RR::DS(rr) => StaticRecord::write_u16_len_prefixed_data(rr, out), @@ -140,6 +146,7 @@ impl From for RR { fn from(aaaa: AAAA) -> RR { RR::AAAA(aaaa) } } impl From for RR { fn from(ns: NS) -> RR { RR::NS(ns) } } impl From for RR { fn from(txt: Txt) -> RR { RR::Txt(txt) } } impl From for RR { fn from(cname: CName) -> RR { RR::CName(cname) } } +impl From for RR { fn from(cname: DName) -> RR { RR::DName(cname) } } impl From for RR { fn from(tlsa: TLSA) -> RR { RR::TLSA(tlsa) } } impl From for RR { fn from(dnskey: DnsKey) -> RR { RR::DnsKey(dnskey) } } impl From for RR { fn from(ds: DS) -> RR { RR::DS(ds) } } @@ -330,6 +337,36 @@ impl StaticRecord for CName { } } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +/// A Delegation Name resource record, referring all queries for subdomains of this name to another +/// subtree of the DNS. +pub struct DName { + /// The name this record is at. + pub name: Name, + /// The delegation name. + /// + /// A resolver should use this domain name tree when looking up any further records for + /// subdomains of [`self.name`]. + pub delegation_name: Name, +} +impl StaticRecord for DName { + const TYPE: u16 = 39; + fn name(&self) -> &Name { &self.name } + fn json(&self) -> String { + format!("{{\"type\":\"dname\",\"name\":\"{}\",\"delegation_name\":\"{}\"}}", + self.name.0, self.delegation_name.0) + } + fn read_from_data(name: Name, mut data: &[u8], wire_packet: &[u8]) -> Result { + Ok(DName { name, delegation_name: read_wire_packet_name(&mut data, wire_packet)? }) + } + fn write_u16_len_prefixed_data(&self, out: &mut Vec) { + let len: u16 = name_len(&self.delegation_name); + out.extend_from_slice(&len.to_be_bytes()); + write_name(out, &self.delegation_name); + } +} + + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] /// A public key resource record which can be used to validate [`RRSig`]s. pub struct DnsKey { diff --git a/src/ser.rs b/src/ser.rs index 2d32973..779bcec 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -102,6 +102,7 @@ pub(crate) fn parse_wire_packet_rr(inp: &mut &[u8], wire_packet: &[u8]) -> Resul NS::TYPE => RR::NS(NS::read_from_data(name, data, wire_packet)?), Txt::TYPE => RR::Txt(Txt::read_from_data(name, data, wire_packet)?), CName::TYPE => RR::CName(CName::read_from_data(name, data, wire_packet)?), + DName::TYPE => RR::DName(DName::read_from_data(name, data, wire_packet)?), TLSA::TYPE => RR::TLSA(TLSA::read_from_data(name, data, wire_packet)?), DnsKey::TYPE => RR::DnsKey(DnsKey::read_from_data(name, data, wire_packet)?), DS::TYPE => RR::DS(DS::read_from_data(name, data, wire_packet)?), diff --git a/src/validation.rs b/src/validation.rs index 8b27fce..c3b134d 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -1,5 +1,6 @@ //! Utilities to deserialize and validate RFC 9102 proofs +use alloc::borrow::ToOwned; use alloc::vec::Vec; use alloc::vec; use core::cmp; @@ -368,14 +369,34 @@ impl<'a> VerifiedRRStream<'a> { /// You MUST still check that the current UNIX time is between /// [`VerifiedRRStream::valid_from`] and [`VerifiedRRStream::expires`] before /// using any records returned here. - pub fn resolve_name<'b>(&self, mut name: &'b Name) -> Vec<&'a RR> where 'a: 'b { + pub fn resolve_name<'b>(&self, name_param: &'b Name) -> Vec<&'a RR> where 'a: 'b { + let mut dname_name; + let mut name = name_param; loop { let mut cname_search = self.verified_rrs.iter() .filter(|rr| rr.name() == name) .filter_map(|rr| if let RR::CName(cn) = rr { Some(cn) } else { None }); if let Some(cname) = cname_search.next() { name = &cname.canonical_name; + continue; } + + let mut dname_search = self.verified_rrs.iter() + .filter(|rr| name.ends_with(&**rr.name())) + .filter_map(|rr| if let RR::DName(dn) = rr { Some(dn) } else { None }); + if let Some(dname) = dname_search.next() { + let prefix = name.strip_suffix(&*dname.name).expect("We just filtered for this"); + let resolved_name = prefix.to_owned() + &dname.delegation_name; + dname_name = if let Ok(name) = resolved_name.try_into() { + name + } else { + // This should only happen if the combined name ended up being too long + return Vec::new(); + }; + name = &dname_name; + continue; + } + return self.verified_rrs.iter().filter(|rr| rr.name() == name).map(|rr| *rr).collect(); } }