X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=src%2Fquery.rs;h=a523db35662f83d3d56b564b11f6c99e668e9897;hb=8b836e295ec81c50907fe1f60ca43c82df792025;hp=548ff153d0ce2e551e112131d8a02777be554e64;hpb=5330d7651e4e1ea59085dacc19291aa94613cb5c;p=dnssec-prover diff --git a/src/query.rs b/src/query.rs index 548ff15..a523db3 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,7 +1,13 @@ //! This module exposes utilities for building DNSSEC proofs by directly querying a recursive //! resolver. +use core::{cmp, ops}; +use alloc::vec; +use alloc::vec::Vec; + +#[cfg(feature = "std")] use std::net::{SocketAddr, TcpStream}; +#[cfg(feature = "std")] use std::io::{Read, Write, Error, ErrorKind}; #[cfg(feature = "tokio")] @@ -9,28 +15,94 @@ use tokio_crate::net::TcpStream as TokioTcpStream; #[cfg(feature = "tokio")] use tokio_crate::io::{AsyncReadExt, AsyncWriteExt}; - -use crate::validation::write_rr; use crate::rr::*; use crate::ser::*; -// We don't care about transaction IDs as we're only going to accept signed data. Thus, we use -// this constant instead of a random value. -const TXID: u16 = 0x4242; - -fn emap(v: Result) -> Result { - v.map_err(|_| Error::new(ErrorKind::Other, "Bad Response")) +// In testing use a rather small buffer to ensure we hit the allocation paths sometimes. In +// production, we should generally never actually need to go to heap as DNS messages are rarely +// larger than a KiB or two. +#[cfg(any(test, fuzzing))] +const STACK_BUF_LIMIT: u16 = 32; +#[cfg(not(any(test, fuzzing)))] +const STACK_BUF_LIMIT: u16 = 2048; + +/// A buffer for storing queries and responses. +#[derive(Clone, PartialEq, Eq)] +pub struct QueryBuf { + buf: [u8; STACK_BUF_LIMIT as usize], + heap_buf: Vec, + len: u16, +} +impl QueryBuf { + /// Generates a new buffer of the given length, consisting of all zeros. + pub fn new_zeroed(len: u16) -> Self { + let heap_buf = if len > STACK_BUF_LIMIT { vec![0; len as usize] } else { Vec::new() }; + Self { + buf: [0; STACK_BUF_LIMIT as usize], + heap_buf, + len + } + } + /// Extends the size of this buffer by appending the given slice. + /// + /// If the total length of this buffer exceeds [`u16::MAX`] after appending, the buffer's state + /// is undefined, however pushing data beyond [`u16::MAX`] will not panic. + pub fn extend_from_slice(&mut self, sl: &[u8]) { + let new_len = self.len.saturating_add(sl.len() as u16); + let was_heap = self.len > STACK_BUF_LIMIT; + let is_heap = new_len > STACK_BUF_LIMIT; + if was_heap != is_heap { + self.heap_buf = vec![0; new_len as usize]; + self.heap_buf[..self.len as usize].copy_from_slice(&self.buf[..self.len as usize]); + } + let target = if is_heap { + self.heap_buf.resize(new_len as usize, 0); + &mut self.heap_buf[self.len as usize..] + } else { + &mut self.buf[self.len as usize..new_len as usize] + }; + target.copy_from_slice(sl); + self.len = new_len; + } + /// Converts this query into its bytes on the heap + pub fn into_vec(self) -> Vec { + if self.len > STACK_BUF_LIMIT { + self.heap_buf + } else { + self.buf[..self.len as usize].to_vec() + } + } +} +impl ops::Deref for QueryBuf { + type Target = [u8]; + fn deref(&self) -> &[u8] { + if self.len > STACK_BUF_LIMIT { + &self.heap_buf + } else { + &self.buf[..self.len as usize] + } + } +} +impl ops::DerefMut for QueryBuf { + fn deref_mut(&mut self) -> &mut [u8] { + if self.len > STACK_BUF_LIMIT { + &mut self.heap_buf + } else { + &mut self.buf[..self.len as usize] + } + } } -fn build_query(domain: Name, ty: u16) -> Vec { - // TODO: Move to not allocating for the query - let mut query = Vec::with_capacity(1024); - let query_msg_len: u16 = 2 + 2 + 8 + 2 + 2 + name_len(&domain) + 11; - query.extend_from_slice(&query_msg_len.to_be_bytes()); +// We don't care about transaction IDs as we're only going to accept signed data. +// Further, if we're querying over DoH, the RFC says we SHOULD use a transaction ID of 0 here. +const TXID: u16 = 0; + +fn build_query(domain: &Name, ty: u16) -> QueryBuf { + let mut query = QueryBuf::new_zeroed(0); query.extend_from_slice(&TXID.to_be_bytes()); query.extend_from_slice(&[0x01, 0x20]); // Flags: Recursive, Authenticated Data query.extend_from_slice(&[0, 1, 0, 0, 0, 0, 0, 1]); // One question, One additional - write_name(&mut query, &domain); + write_name(&mut query, domain); query.extend_from_slice(&ty.to_be_bytes()); query.extend_from_slice(&1u16.to_be_bytes()); // INternet class query.extend_from_slice(&[0, 0, 0x29]); // . OPT @@ -41,157 +113,318 @@ fn build_query(domain: Name, ty: u16) -> Vec { query } -fn send_query(stream: &mut TcpStream, domain: Name, ty: u16) -> Result<(), Error> { - let query = build_query(domain, ty); - stream.write_all(&query)?; - Ok(()) +#[cfg(fuzzing)] +/// Read some input and parse it as if it came from a server, for fuzzing. +pub fn fuzz_response(response: &[u8]) { + let (mut proof, mut names) = (Vec::new(), Vec::new()); + let _ = handle_response(response, &mut proof, &mut names); } -#[cfg(feature = "tokio")] -async fn send_query_async(stream: &mut TokioTcpStream, domain: Name, ty: u16) -> Result<(), Error> { - let query = build_query(domain, ty); - stream.write_all(&query).await?; - Ok(()) -} - -fn handle_response(resp: &[u8], proof: &mut Vec) -> Result, Error> { +fn handle_response(resp: &[u8], proof: &mut Vec, rrsig_key_names: &mut Vec) -> Result { let mut read: &[u8] = resp; - if emap(read_u16(&mut read))? != TXID { return Err(Error::new(ErrorKind::Other, "bad txid")); } + if read_u16(&mut read)? != TXID { return Err(()); } // 2 byte transaction ID - let flags = emap(read_u16(&mut read))?; + let flags = read_u16(&mut read)?; if flags & 0b1000_0000_0000_0000 == 0 { - return Err(Error::new(ErrorKind::Other, "Missing response flag")); + return Err(()); } if flags & 0b0111_1010_0000_0111 != 0 { - return Err(Error::new(ErrorKind::Other, "Server indicated error or provided bunk flags")); + return Err(()); } if flags & 0b10_0000 == 0 { - return Err(Error::new(ErrorKind::Other, "Server indicated data could not be authenticated")); + return Err(()); } - let questions = emap(read_u16(&mut read))?; - if questions != 1 { return Err(Error::new(ErrorKind::Other, "server responded to multiple Qs")); } - let answers = emap(read_u16(&mut read))?; - let _authorities = emap(read_u16(&mut read))?; - let _additional = emap(read_u16(&mut read))?; + let questions = read_u16(&mut read)?; + if questions != 1 { return Err(()); } + let answers = read_u16(&mut read)?; + if answers == 0 { return Err(()); } + let _authorities = read_u16(&mut read)?; + let _additional = read_u16(&mut read)?; for _ in 0..questions { - emap(read_wire_packet_name(&mut read, resp))?; - emap(read_u16(&mut read))?; // type - emap(read_u16(&mut read))?; // class + read_wire_packet_name(&mut read, resp)?; + read_u16(&mut read)?; // type + read_u16(&mut read)?; // class } // Only read the answers (skip authorities and additional) as that's all we care about. - let mut rrsig_opt = None; + let mut min_ttl = u32::MAX; for _ in 0..answers { - let (rr, ttl) = emap(parse_wire_packet_rr(&mut read, &resp))?; + let (rr, ttl) = parse_wire_packet_rr(&mut read, &resp)?; write_rr(&rr, ttl, proof); - if let RR::RRSig(rrsig) = rr { rrsig_opt = Some(rrsig); } + min_ttl = cmp::min(min_ttl, ttl); + if let RR::RRSig(rrsig) = rr { rrsig_key_names.push(rrsig.key_name); } + } + Ok(min_ttl) +} + +#[cfg(fuzzing)] +/// Read a stream of responses and handle them it as if they came from a server, for fuzzing. +pub fn fuzz_proof_builder(mut response_stream: &[u8]) { + let (mut builder, _) = ProofBuilder::new(&"example.com.".try_into().unwrap(), Txt::TYPE); + while builder.awaiting_responses() { + let len = if let Ok(len) = read_u16(&mut response_stream) { len } else { return }; + let mut buf = QueryBuf::new_zeroed(len); + if response_stream.len() < len as usize { return; } + buf.copy_from_slice(&response_stream[..len as usize]); + response_stream = &response_stream[len as usize..]; + let _ = builder.process_response(&buf); } - Ok(rrsig_opt) + let _ = builder.finish_proof(); +} + +const MAX_REQUESTS: usize = 10; +/// A simple state machine which will generate a series of queries and process the responses until +/// it has built a DNSSEC proof. +/// +/// A [`ProofBuilder`] driver starts with [`ProofBuilder::new`], fetching the state machine and +/// initial query. As long as [`ProofBuilder::awaiting_responses`] returns true, responses should +/// be read from the resolver. For each query response read from the DNS resolver, +/// [`ProofBuilder::process_response`] should be called, and each fresh query returned should be +/// sent to the resolver. Once [`ProofBuilder::awaiting_responses`] returns false, +/// [`ProofBuilder::finish_proof`] should be called to fetch the resulting proof. +/// +/// To build a DNSSEC proof using a DoH server, take each [`QueryBuf`], encode it as base64url, and +/// make a query to `https://doh-server/endpoint?dns=base64url_encoded_query` with an `Accept` +/// header of `application/dns-message`. Each response, in raw binary, can be fed directly into +/// [`ProofBuilder::process_response`]. +pub struct ProofBuilder { + proof: Vec, + min_ttl: u32, + dnskeys_requested: Vec, + pending_queries: usize, + queries_made: usize, } -fn read_response(stream: &mut TcpStream, proof: &mut Vec) -> Result, Error> { - let mut len = [0; 2]; - stream.read_exact(&mut len)?; - let mut resp = vec![0; u16::from_be_bytes(len) as usize]; - stream.read_exact(&mut resp)?; - handle_response(&resp, proof) +impl ProofBuilder { + /// Constructs a new [`ProofBuilder`] and an initial query to send to the recursive resolver to + /// begin the proof building process. + /// + /// Given a correctly-functioning resolver the proof will ultimately be able to prove the + /// contents of any records with the given `ty`pe at the given `name` (as long as the given + /// `ty`pe is supported by this library). + /// + /// You can find constants for supported standard types in the [`crate::rr`] module. + pub fn new(name: &Name, ty: u16) -> (ProofBuilder, QueryBuf) { + let initial_query = build_query(name, ty); + (ProofBuilder { + proof: Vec::new(), + min_ttl: u32::MAX, + dnskeys_requested: Vec::with_capacity(MAX_REQUESTS), + pending_queries: 1, + queries_made: 1, + }, initial_query) + } + + /// Returns true as long as further responses are expected from the resolver. + /// + /// As long as this returns true, responses should be read from the resolver and passed to + /// [`Self::process_response`]. Once this returns false, [`Self::finish_proof`] should be used + /// to (possibly) get the final proof. + pub fn awaiting_responses(&self) -> bool { + self.pending_queries > 0 && self.queries_made <= MAX_REQUESTS + } + + /// Processes a query response from the recursive resolver, returning a list of new queries to + /// send to the resolver. + pub fn process_response(&mut self, resp: &QueryBuf) -> Result, ()> { + if self.pending_queries == 0 { return Err(()); } + + let mut rrsig_key_names = Vec::new(); + let min_ttl = handle_response(&resp, &mut self.proof, &mut rrsig_key_names)?; + self.min_ttl = cmp::min(self.min_ttl, min_ttl); + self.pending_queries -= 1; + + rrsig_key_names.sort_unstable(); + rrsig_key_names.dedup(); + + let mut new_queries = Vec::with_capacity(2); + for key_name in rrsig_key_names.drain(..) { + if !self.dnskeys_requested.contains(&key_name) { + new_queries.push(build_query(&key_name, DnsKey::TYPE)); + self.pending_queries += 1; + self.queries_made += 1; + self.dnskeys_requested.push(key_name.clone()); + + if key_name.as_str() != "." { + new_queries.push(build_query(&key_name, DS::TYPE)); + self.pending_queries += 1; + self.queries_made += 1; + } + } + } + if self.queries_made <= MAX_REQUESTS { + Ok(new_queries) + } else { + Ok(Vec::new()) + } + } + + /// Finalizes the proof, if one is available, and returns it as well as the TTL that should be + /// used to cache the proof (i.e. the lowest TTL of all records which were used to build the + /// proof). + pub fn finish_proof(self) -> Result<(Vec, u32), ()> { + if self.pending_queries > 0 || self.queries_made > MAX_REQUESTS { + Err(()) + } else { + Ok((self.proof, self.min_ttl)) + } + } +} + +#[cfg(feature = "std")] +fn send_query(stream: &mut TcpStream, query: &[u8]) -> Result<(), Error> { + stream.write_all(&(query.len() as u16).to_be_bytes())?; + stream.write_all(&query)?; + Ok(()) +} + +#[cfg(feature = "tokio")] +async fn send_query_async(stream: &mut TokioTcpStream, query: &[u8]) -> Result<(), Error> { + stream.write_all(&(query.len() as u16).to_be_bytes()).await?; + stream.write_all(&query).await?; + Ok(()) +} + +#[cfg(feature = "std")] +fn read_response(stream: &mut TcpStream) -> Result { + let mut len_bytes = [0; 2]; + stream.read_exact(&mut len_bytes)?; + let mut buf = QueryBuf::new_zeroed(u16::from_be_bytes(len_bytes)); + stream.read_exact(&mut buf)?; + Ok(buf) } #[cfg(feature = "tokio")] -async fn read_response_async(stream: &mut TokioTcpStream, proof: &mut Vec) -> Result, Error> { - let mut len = [0; 2]; - stream.read_exact(&mut len).await?; - let mut resp = vec![0; u16::from_be_bytes(len) as usize]; - stream.read_exact(&mut resp).await?; - handle_response(&resp, proof) +async fn read_response_async(stream: &mut TokioTcpStream) -> Result { + let mut len_bytes = [0; 2]; + stream.read_exact(&mut len_bytes).await?; + let mut buf = QueryBuf::new_zeroed(u16::from_be_bytes(len_bytes)); + stream.read_exact(&mut buf).await?; + Ok(buf) } +#[cfg(feature = "std")] macro_rules! build_proof_impl { - ($stream: ident, $send_query: ident, $read_response: ident $(, $async_ok: tt)?) => { { - let mut res = Vec::new(); - let mut reached_root = false; - for i in 0..10 { - let rrsig_opt = $read_response(&mut $stream, &mut res) + ($stream: ident, $send_query: ident, $read_response: ident, $domain: expr, $ty: expr $(, $async_ok: tt)?) => { { + // We require the initial query to have already gone out, and assume our resolver will + // return any CNAMEs all the way to the final record in the response. From there, we just + // have to take any RRSIGs in the response and walk them up to the root. We do so + // iteratively, sending DNSKEY and DS lookups after every response, deduplicating requests + // using `dnskeys_requested`. + let (mut builder, initial_query) = ProofBuilder::new($domain, $ty); + $send_query(&mut $stream, &initial_query) + $(.await?; $async_ok)??; // Either await?; Ok(())?, or just ? + while builder.awaiting_responses() { + let response = $read_response(&mut $stream) $(.await?; $async_ok)??; // Either await?; Ok(())?, or just ? - if let Some(rrsig) = rrsig_opt { - if rrsig.name.as_str() == "." { - reached_root = true; - } else { - if i != 0 && rrsig.name == rrsig.key_name { - $send_query(&mut $stream, rrsig.key_name, DS::TYPE) - } else { - $send_query(&mut $stream, rrsig.key_name, DnsKey::TYPE) - }$(.await?; $async_ok)??; // Either await?; Ok(())?, or just ? - } + let new_queries = builder.process_response(&response) + .map_err(|()| Error::new(ErrorKind::Other, "Bad response"))?; + for query in new_queries { + $send_query(&mut $stream, &query) + $(.await?; $async_ok)??; // Either await?; Ok(())?, or just ? } - if reached_root { break; } } - if !reached_root { Err(Error::new(ErrorKind::Other, "Too many requests required")) } - else { Ok(res) } + builder.finish_proof() + .map_err(|()| Error::new(ErrorKind::Other, "Too many requests required")) } } } -fn build_proof(resolver: SocketAddr, domain: Name, ty: u16) -> Result, Error> { +#[cfg(feature = "std")] +fn build_proof(resolver: SocketAddr, domain: &Name, ty: u16) -> Result<(Vec, u32), Error> { let mut stream = TcpStream::connect(resolver)?; - send_query(&mut stream, domain, ty)?; - build_proof_impl!(stream, send_query, read_response) + build_proof_impl!(stream, send_query, read_response, domain, ty) } #[cfg(feature = "tokio")] -async fn build_proof_async(resolver: SocketAddr, domain: Name, ty: u16) -> Result, Error> { +async fn build_proof_async(resolver: SocketAddr, domain: &Name, ty: u16) -> Result<(Vec, u32), Error> { let mut stream = TokioTcpStream::connect(resolver).await?; - send_query_async(&mut stream, domain, ty).await?; - build_proof_impl!(stream, send_query_async, read_response_async, { Ok::<(), Error>(()) }) + build_proof_impl!(stream, send_query_async, read_response_async, domain, ty, { Ok::<(), Error>(()) }) } -/// Builds a DNSSEC proof for an A record by querying a recursive resolver -pub fn build_a_proof(resolver: SocketAddr, domain: Name) -> Result, Error> { +/// Builds a DNSSEC proof for an A record by querying a recursive resolver, returning the proof as +/// well as the TTL for the proof provided by the recursive resolver. +/// +/// Note that this proof is NOT verified in any way, you need to use the [`crate::validation`] +/// module to validate the records contained. +#[cfg(feature = "std")] +pub fn build_a_proof(resolver: SocketAddr, domain: &Name) -> Result<(Vec, u32), Error> { build_proof(resolver, domain, A::TYPE) } -/// Builds a DNSSEC proof for an AAAA record by querying a recursive resolver -pub fn build_aaaa_proof(resolver: SocketAddr, domain: Name) -> Result, Error> { +/// Builds a DNSSEC proof for an AAAA record by querying a recursive resolver, returning the proof +/// as well as the TTL for the proof provided by the recursive resolver. +/// +/// Note that this proof is NOT verified in any way, you need to use the [`crate::validation`] +/// module to validate the records contained. +#[cfg(feature = "std")] +pub fn build_aaaa_proof(resolver: SocketAddr, domain: &Name) -> Result<(Vec, u32), Error> { build_proof(resolver, domain, AAAA::TYPE) } -/// Builds a DNSSEC proof for a TXT record by querying a recursive resolver -pub fn build_txt_proof(resolver: SocketAddr, domain: Name) -> Result, Error> { +/// Builds a DNSSEC proof for an TXT record by querying a recursive resolver, returning the proof +/// as well as the TTL for the proof provided by the recursive resolver. +/// +/// Note that this proof is NOT verified in any way, you need to use the [`crate::validation`] +/// module to validate the records contained. +#[cfg(feature = "std")] +pub fn build_txt_proof(resolver: SocketAddr, domain: &Name) -> Result<(Vec, u32), Error> { build_proof(resolver, domain, Txt::TYPE) } -/// Builds a DNSSEC proof for a TLSA record by querying a recursive resolver -pub fn build_tlsa_proof(resolver: SocketAddr, domain: Name) -> Result, Error> { +/// Builds a DNSSEC proof for an TLSA record by querying a recursive resolver, returning the proof +/// as well as the TTL for the proof provided by the recursive resolver. +/// +/// Note that this proof is NOT verified in any way, you need to use the [`crate::validation`] +/// module to validate the records contained. +#[cfg(feature = "std")] +pub fn build_tlsa_proof(resolver: SocketAddr, domain: &Name) -> Result<(Vec, u32), Error> { build_proof(resolver, domain, TLSA::TYPE) } -/// Builds a DNSSEC proof for an A record by querying a recursive resolver +/// Builds a DNSSEC proof for an A record by querying a recursive resolver, returning the proof as +/// well as the TTL for the proof provided by the recursive resolver. +/// +/// Note that this proof is NOT verified in any way, you need to use the [`crate::validation`] +/// module to validate the records contained. #[cfg(feature = "tokio")] -pub async fn build_a_proof_async(resolver: SocketAddr, domain: Name) -> Result, Error> { +pub async fn build_a_proof_async(resolver: SocketAddr, domain: &Name) -> Result<(Vec, u32), Error> { build_proof_async(resolver, domain, A::TYPE).await } -/// Builds a DNSSEC proof for an AAAA record by querying a recursive resolver +/// Builds a DNSSEC proof for an AAAA record by querying a recursive resolver, returning the proof +/// as well as the TTL for the proof provided by the recursive resolver. +/// +/// Note that this proof is NOT verified in any way, you need to use the [`crate::validation`] +/// module to validate the records contained. #[cfg(feature = "tokio")] -pub async fn build_aaaa_proof_async(resolver: SocketAddr, domain: Name) -> Result, Error> { +pub async fn build_aaaa_proof_async(resolver: SocketAddr, domain: &Name) -> Result<(Vec, u32), Error> { build_proof_async(resolver, domain, AAAA::TYPE).await } -/// Builds a DNSSEC proof for a TXT record by querying a recursive resolver +/// Builds a DNSSEC proof for an TXT record by querying a recursive resolver, returning the proof +/// as well as the TTL for the proof provided by the recursive resolver. +/// +/// Note that this proof is NOT verified in any way, you need to use the [`crate::validation`] +/// module to validate the records contained. #[cfg(feature = "tokio")] -pub async fn build_txt_proof_async(resolver: SocketAddr, domain: Name) -> Result, Error> { +pub async fn build_txt_proof_async(resolver: SocketAddr, domain: &Name) -> Result<(Vec, u32), Error> { build_proof_async(resolver, domain, Txt::TYPE).await } -/// Builds a DNSSEC proof for a TLSA record by querying a recursive resolver +/// Builds a DNSSEC proof for an TLSA record by querying a recursive resolver, returning the proof +/// as well as the TTL for the proof provided by the recursive resolver. +/// +/// Note that this proof is NOT verified in any way, you need to use the [`crate::validation`] +/// module to validate the records contained. #[cfg(feature = "tokio")] -pub async fn build_tlsa_proof_async(resolver: SocketAddr, domain: Name) -> Result, Error> { +pub async fn build_tlsa_proof_async(resolver: SocketAddr, domain: &Name) -> Result<(Vec, u32), Error> { build_proof_async(resolver, domain, TLSA::TYPE).await } -#[cfg(test)] +#[cfg(all(feature = "validation", feature = "std", test))] mod tests { use super::*; use crate::validation::*; @@ -201,12 +434,11 @@ mod tests { use std::net::ToSocketAddrs; use std::time::SystemTime; - #[test] fn test_cloudflare_txt_query() { let sockaddr = "8.8.8.8:53".to_socket_addrs().unwrap().next().unwrap(); let query_name = "cloudflare.com.".try_into().unwrap(); - let proof = build_txt_proof(sockaddr, query_name).unwrap(); + let (proof, _) = build_txt_proof(sockaddr, &query_name).unwrap(); let mut rrs = parse_rr_stream(&proof).unwrap(); rrs.shuffle(&mut rand::rngs::OsRng); @@ -218,11 +450,27 @@ mod tests { assert!(verified_rrs.expires > now); } + #[test] + fn test_sha1_query() { + let sockaddr = "8.8.8.8:53".to_socket_addrs().unwrap().next().unwrap(); + let query_name = "benthecarman.com.".try_into().unwrap(); + let (proof, _) = build_a_proof(sockaddr, &query_name).unwrap(); + + let mut rrs = parse_rr_stream(&proof).unwrap(); + rrs.shuffle(&mut rand::rngs::OsRng); + let verified_rrs = verify_rr_stream(&rrs).unwrap(); + assert!(verified_rrs.verified_rrs.len() >= 1); + + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); + assert!(verified_rrs.valid_from < now); + assert!(verified_rrs.expires > now); + } + #[test] fn test_txt_query() { let sockaddr = "8.8.8.8:53".to_socket_addrs().unwrap().next().unwrap(); let query_name = "matt.user._bitcoin-payment.mattcorallo.com.".try_into().unwrap(); - let proof = build_txt_proof(sockaddr, query_name).unwrap(); + let (proof, _) = build_txt_proof(sockaddr, &query_name).unwrap(); let mut rrs = parse_rr_stream(&proof).unwrap(); rrs.shuffle(&mut rand::rngs::OsRng); @@ -234,6 +482,31 @@ mod tests { assert!(verified_rrs.expires > now); } + #[test] + fn test_cname_query() { + 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 = "cname_test.matcorallo.com.".try_into().unwrap(); + let (proof, _) = build_txt_proof(sockaddr, &query_name).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(), 2); + + 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(), "txt_test.matcorallo.com."); + assert_eq!(txt.data, b"dnssec_prover_test"); + } else { panic!(); } + } + } + #[cfg(feature = "tokio")] use tokio_crate as tokio; @@ -242,7 +515,7 @@ mod tests { async fn test_txt_query_async() { let sockaddr = "8.8.8.8:53".to_socket_addrs().unwrap().next().unwrap(); let query_name = "matt.user._bitcoin-payment.mattcorallo.com.".try_into().unwrap(); - let proof = build_txt_proof_async(sockaddr, query_name).await.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); @@ -253,4 +526,57 @@ mod tests { assert!(verified_rrs.valid_from < now); assert!(verified_rrs.expires > now); } + + #[cfg(feature = "tokio")] + #[tokio::test] + async fn test_cross_domain_cname_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.x_domain_cname_wild.matcorallo.com.".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(), 2); + + 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(), "matt.user._bitcoin-payment.mattcorallo.com."); + assert!(txt.data.starts_with(b"bitcoin:")); + } 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!(); } + } + } + }