feat: DNS client + DANE impl with Hickory

This commit is contained in:
DarkCat09 2024-08-14 17:30:31 +04:00
parent 2a097fb800
commit 5b43635b62
Signed by: DarkCat09
GPG key ID: 0A26CD5B3345D6E3
7 changed files with 734 additions and 9 deletions

View file

@ -18,6 +18,8 @@ pub const SHA512_B64_LEN: usize = 88; // 4 * ((512 / 8) as f64 / 3 as f64).ceil(
pub enum HashAlgo {
Sha256,
Sha512,
/// Do not hash, compare the whole cert
Raw,
}
/// Structure holding a TLS cert hash

117
src/dns.rs Normal file
View file

@ -0,0 +1,117 @@
use std::net::IpAddr;
use bytes::Bytes;
use hickory_client::{
client::{AsyncClient, ClientHandle},
proto::iocompat::AsyncIoTokioAsStd,
rr::{
rdata::tlsa::{CertUsage, Matching, Selector},
DNSClass, IntoName, RData, RecordType,
},
tcp::TcpClientStream,
};
use tokio::net::ToSocketAddrs;
use crate::{certs::fingerprint::HashAlgo, LibError};
pub struct DnsClient(AsyncClient);
impl DnsClient {
pub async fn init(server: impl ToSocketAddrs) -> Result<Self, LibError> {
for addr in tokio::net::lookup_host(server).await? {
let (stream, sender) =
TcpClientStream::<AsyncIoTokioAsStd<tokio::net::TcpStream>>::new(addr);
if let Ok((client, bg)) = AsyncClient::new(stream, sender, None).await {
tokio::spawn(bg);
return Ok(DnsClient(client));
} else {
continue;
}
}
Err(LibError::HostLookupError)
}
pub async fn query_ipv4(
&mut self,
name: &str,
) -> Result<impl Iterator<Item = IpAddr>, LibError> {
self.query_ip(name, RecordType::A).await
}
pub async fn query_ipv6(
&mut self,
name: &str,
) -> Result<impl Iterator<Item = IpAddr>, LibError> {
self.query_ip(name, RecordType::AAAA).await
}
#[inline]
async fn query_ip(
&mut self,
name: &str,
rtype: RecordType,
) -> Result<impl Iterator<Item = IpAddr>, LibError> {
let answers = self
.0
.query(name.into_name()?, DNSClass::IN, rtype)
.await?
.into_message()
.take_answers();
if !answers.is_empty() {
Ok(answers.into_iter().filter_map(|rec| match rec.data() {
Some(RData::A(addr)) => Some(IpAddr::V4(addr.0)),
Some(RData::AAAA(addr)) => Some(IpAddr::V6(addr.0)),
_ => None,
}))
} else {
Err(LibError::HostLookupError)
}
}
pub async fn query_tlsa(
&mut self,
domain: &str,
port: u16,
) -> Result<impl Iterator<Item = (HashAlgo, Bytes)>, LibError> {
let answers = self
.0
.query(
format!("_{}._tcp.{}", port, domain).into_name()?,
DNSClass::IN,
RecordType::TLSA,
)
.await?
.into_message()
.take_answers();
if !answers.is_empty() {
Ok(answers.into_iter().filter_map(|rec| {
if let Some(RData::TLSA(tlsa)) = rec.data() {
if tlsa.cert_usage() == CertUsage::DomainIssued
&& tlsa.selector() == Selector::Spki
{
let hash_algo = match tlsa.matching() {
Matching::Sha256 => HashAlgo::Sha256,
Matching::Sha512 => HashAlgo::Sha512,
Matching::Raw => HashAlgo::Raw,
_ => {
return None;
}
};
// TODO: optimize?
// if tlsa.cert_data() returned inner Vec<u8>,
// i could do this with zero-copy
Some((hash_algo, Bytes::copy_from_slice(tlsa.cert_data())))
} else {
None
}
} else {
None
}
}))
} else {
Err(LibError::HostLookupError)
}
}
}

View file

@ -1,5 +1,10 @@
//! Library error structures and enums
#[cfg(feature = "hickory")]
use hickory_client::{
error::ClientError as HickoryClientError, proto::error::ProtoError as HickoryProtoError,
};
/// Main error structure, also a wrapper for everything else
#[derive(Debug)]
pub enum LibError {
@ -8,6 +13,8 @@ pub enum LibError {
IoError(std::io::Error),
/// URL parse or check error
InvalidUrlError(InvalidUrl),
///
HostLookupError,
/// Response status code is out of [10; 69] range
StatusOutOfRange(u8),
/// Response metadata or content cannot be parsed
@ -15,6 +22,12 @@ pub enum LibError {
DataNotUtf8(std::string::FromUtf8Error),
/// Provided string is not a valid MIME type
InvalidMime(mime::FromStrError),
///
#[cfg(feature = "hickory")]
DnsClientError(HickoryClientError),
///
#[cfg(feature = "hickory")]
DnsProtoError(HickoryProtoError),
}
impl From<std::io::Error> for LibError {
@ -59,6 +72,22 @@ impl From<mime::FromStrError> for LibError {
}
}
#[cfg(feature = "hickory")]
impl From<HickoryClientError> for LibError {
#[inline]
fn from(err: HickoryClientError) -> Self {
Self::DnsClientError(err)
}
}
#[cfg(feature = "hickory")]
impl From<HickoryProtoError> for LibError {
#[inline]
fn from(err: HickoryProtoError) -> Self {
Self::DnsProtoError(err)
}
}
/// URL parse or check error
#[derive(Debug)]
pub enum InvalidUrl {

View file

@ -6,6 +6,9 @@ pub mod client;
pub mod error;
pub mod status;
#[cfg(feature = "hickory")]
pub mod dns;
pub use client::Client;
pub use error::*;
pub use status::*;