Add support for DNAME resolution
authorMatt Corallo <git@bluematt.me>
Mon, 12 Feb 2024 04:21:45 +0000 (04:21 +0000)
committerMatt Corallo <git@bluematt.me>
Mon, 12 Feb 2024 05:07:04 +0000 (05:07 +0000)
src/query.rs
src/rr.rs
src/ser.rs
src/validation.rs

index bb8fd7d43db725c7f95ec275535c76b496ba2ce4..a523db35662f83d3d56b564b11f6c99e668e9897 100644 (file)
@@ -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!(); }
+               }
+       }
+
 }
index 4761ddd2041856daccdfb13164d1a07343f3a847..c3367a642d3f38044b9226ad20e80360a4fb5a92 100644 (file)
--- 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<AAAA> for RR { fn from(aaaa: AAAA) -> RR { RR::AAAA(aaaa) } }
 impl From<NS> for RR { fn from(ns: NS) -> RR { RR::NS(ns) } }
 impl From<Txt> for RR { fn from(txt: Txt) -> RR { RR::Txt(txt) } }
 impl From<CName> for RR { fn from(cname: CName) -> RR { RR::CName(cname) } }
+impl From<DName> for RR { fn from(cname: DName) -> RR { RR::DName(cname) } }
 impl From<TLSA> for RR { fn from(tlsa: TLSA) -> RR { RR::TLSA(tlsa) } }
 impl From<DnsKey> for RR { fn from(dnskey: DnsKey) -> RR { RR::DnsKey(dnskey) } }
 impl From<DS> 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<Self, ()> {
+               Ok(DName { name, delegation_name: read_wire_packet_name(&mut data, wire_packet)? })
+       }
+       fn write_u16_len_prefixed_data(&self, out: &mut Vec<u8>) {
+               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 {
index 2d329733b3fc5e8b4016668baab0503d11c0a621..779bcec194ffa915a04906623d0ee48818b97ed7 100644 (file)
@@ -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)?),
index 8b27fce3ad8e31dfbde8d67ba3f22600d0510b21..c3b134db9e812050e7ead6d51db068d10425a129 100644 (file)
@@ -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();
                }
        }