// this constant instead of a random value.
const TXID: u16 = 0x4242;
-fn emap<V>(v: Result<V, ()>) -> Result<V, Error> {
- v.map_err(|_| Error::new(ErrorKind::Other, "Bad Response"))
-}
-
fn build_query(domain: &Name, ty: u16) -> Vec<u8> {
// TODO: Move to not allocating for the query
let mut query = Vec::with_capacity(1024);
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(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(())
+#[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);
}
-fn handle_response(resp: &[u8], proof: &mut Vec<u8>) -> Result<Option<(RRSig, u32)>, Error> {
+fn handle_response(resp: &[u8], proof: &mut Vec<u8>, rrsig_key_names: &mut Vec<Name>) -> Result<u32, ()> {
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))?;
- if answers == 0 { return Err(Error::new(ErrorKind::Other, "No answers")); }
- 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);
min_ttl = cmp::min(min_ttl, ttl);
- if let RR::RRSig(rrsig) = rr { rrsig_opt = Some(rrsig); }
+ if let RR::RRSig(rrsig) = rr { rrsig_key_names.push(rrsig.key_name); }
}
- Ok(rrsig_opt.map(|rr| (rr, min_ttl)))
+ Ok(min_ttl)
}
-fn read_response(stream: &mut TcpStream, proof: &mut Vec<u8>) -> Result<Option<(RRSig, u32)>, 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)
+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.
+pub struct ProofBuilder {
+ proof: Vec<u8>,
+ min_ttl: u32,
+ dnskeys_requested: Vec<Name>,
+ pending_queries: usize,
+ queries_made: usize,
+}
+
+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, Vec<u8>) {
+ 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: &[u8]) -> Result<Vec<Vec<u8>>, ()> {
+ 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<u8>, u32), ()> {
+ if self.pending_queries > 0 || self.queries_made > MAX_REQUESTS {
+ Err(())
+ } else {
+ Ok((self.proof, self.min_ttl))
+ }
+ }
+}
+
+fn send_query(stream: &mut TcpStream, query: &[u8]) -> Result<(), Error> {
+ stream.write_all(&query)?;
+ Ok(())
+}
+
+#[cfg(feature = "tokio")]
+async fn send_query_async(stream: &mut TokioTcpStream, query: &[u8]) -> Result<(), Error> {
+ stream.write_all(&query).await?;
+ Ok(())
+}
+
+type MsgBuf = [u8; u16::MAX as usize];
+
+fn read_response(stream: &mut TcpStream, response_buf: &mut MsgBuf) -> Result<u16, Error> {
+ let mut len_bytes = [0; 2];
+ stream.read_exact(&mut len_bytes)?;
+ let len = u16::from_be_bytes(len_bytes);
+ stream.read_exact(&mut response_buf[..len as usize])?;
+ Ok(len)
}
#[cfg(feature = "tokio")]
-async fn read_response_async(stream: &mut TokioTcpStream, proof: &mut Vec<u8>) -> Result<Option<(RRSig, u32)>, 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, response_buf: &mut MsgBuf) -> Result<u16, Error> {
+ let mut len_bytes = [0; 2];
+ stream.read_exact(&mut len_bytes).await?;
+ let len = u16::from_be_bytes(len_bytes);
+ stream.read_exact(&mut response_buf[..len as usize]).await?;
+ Ok(len)
}
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;
- let mut min_ttl = u32::MAX;
- for i in 0..10 {
- let resp_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 ?
+ let mut response_buf = [0; u16::MAX as usize];
+ while builder.awaiting_responses() {
+ let response_len = $read_response(&mut $stream, &mut response_buf)
$(.await?; $async_ok)??; // Either await?; Ok(())?, or just ?
- if let Some((rrsig, rrsig_min_ttl)) = resp_opt {
- min_ttl = cmp::min(min_ttl, rrsig_min_ttl);
- 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_buf[..response_len as usize])
+ .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, min_ttl)) }
+ builder.finish_proof()
+ .map_err(|()| Error::new(ErrorKind::Other, "Too many requests required"))
} }
}
fn build_proof(resolver: SocketAddr, domain: &Name, ty: u16) -> Result<(Vec<u8>, 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<(Vec<u8>, 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, 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.
pub fn build_a_proof(resolver: SocketAddr, domain: &Name) -> Result<(Vec<u8>, u32), Error> {
build_proof(resolver, domain, A::TYPE)
}
/// 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.
pub fn build_aaaa_proof(resolver: SocketAddr, domain: &Name) -> Result<(Vec<u8>, u32), Error> {
build_proof(resolver, domain, AAAA::TYPE)
}
/// 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.
pub fn build_txt_proof(resolver: SocketAddr, domain: &Name) -> Result<(Vec<u8>, u32), Error> {
build_proof(resolver, domain, Txt::TYPE)
}
/// 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.
pub fn build_tlsa_proof(resolver: SocketAddr, domain: &Name) -> Result<(Vec<u8>, u32), Error> {
build_proof(resolver, domain, TLSA::TYPE)
}
/// 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<(Vec<u8>, u32), Error> {
build_proof_async(resolver, domain, A::TYPE).await
/// 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<(Vec<u8>, u32), Error> {
build_proof_async(resolver, domain, AAAA::TYPE).await
/// 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<(Vec<u8>, u32), Error> {
build_proof_async(resolver, domain, Txt::TYPE).await
/// 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<(Vec<u8>, u32), Error> {
build_proof_async(resolver, domain, TLSA::TYPE).await
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;
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!(); }
+ }
+ }
}