feat/refactor: add DANE verification

should work, but most probably does not
This commit is contained in:
DarkCat09 2024-08-16 21:16:40 +04:00
parent 05f22493be
commit dc126d2e0f
Signed by: DarkCat09
GPG key ID: 0A26CD5B3345D6E3
3 changed files with 146 additions and 32 deletions

View file

@ -1,4 +1,10 @@
use std::{borrow::Cow, io::Write, os::fd::AsFd, path::Path, sync::Mutex};
use std::{
borrow::Cow,
io::{BufWriter, Write},
os::fd::AsFd,
path::Path,
sync::Mutex,
};
use dashmap::DashMap;
use tokio::io::AsyncBufReadExt;
@ -12,6 +18,8 @@ use crate::{
pub struct FileBasedCertVerifier {
fd: Mutex<std::os::fd::OwnedFd>,
map: DashMap<String, SelfsignedCert>,
#[cfg(feature = "hickory")]
dns: Option<crate::dns::DnsClient>,
}
impl FileBasedCertVerifier {
@ -85,7 +93,103 @@ impl FileBasedCertVerifier {
.try_clone_to_owned()?,
);
Ok(FileBasedCertVerifier { fd, map })
Ok(FileBasedCertVerifier {
fd,
map,
#[cfg(feature = "hickory")]
dns: None,
})
}
}
impl FileBasedCertVerifier {
pub fn add_trusted_cert(
&self,
host: &str,
hash: CertFingerprint,
) -> Result<(), std::io::Error> {
let fp = hash.base64();
let ft = hash.fingerprint_type_str();
// TODO: remove eprintln!()
eprintln!("Warning: adding {} cert with FP {}", &host, &fp);
self.map.insert(
host.to_owned(),
SelfsignedCert {
fingerprint: hash,
expires: 0, // TODO after implementing cert parsing in tokio-gemini
},
);
// trick with cloning file descriptor
// because we are not allowed to mutate &self
let f = std::fs::File::from(self.fd.lock().unwrap().try_clone()?);
let mut bw = BufWriter::new(f);
bw.write_all(host.as_bytes())?;
bw.write_all(b" 0 ")?; // TODO after implementing `expires`
bw.write_all(ft.as_bytes())?;
bw.write_all(b" ")?;
bw.write_all(fp.as_bytes())?;
bw.write_all(b"\n")?;
bw.flush()?;
Ok(())
}
#[cfg(feature = "hickory")]
pub fn dane(
&self,
cert: &CertificateDer,
host: &str,
port: u16,
) -> Result<CertFingerprint, LibError> {
let mut dns = if let Some(dns) = &self.dns {
dns.clone()
} else {
return Err(LibError::HostLookupError);
};
let rt = tokio::runtime::Handle::try_current()?;
let mut sha256: Option<CertFingerprint> = None;
let mut sha512: Option<CertFingerprint> = None;
for tlsa_fp in rt.block_on(dns.query_tlsa(host, port))? {
match tlsa_fp {
CertFingerprint::Sha256(_) => {
if sha256.is_none() {
sha256 = Some(CertFingerprint::new_sha256(cert));
}
let this_fp = sha256.as_ref().unwrap();
if this_fp == &tlsa_fp {
return Ok(sha256.unwrap());
}
}
CertFingerprint::Sha512(_) => {
if sha512.is_none() {
sha512 = Some(CertFingerprint::new_sha512(cert));
}
let this_fp = sha512.as_ref().unwrap();
if this_fp == &tlsa_fp {
return Ok(sha512.unwrap());
}
}
CertFingerprint::Raw(_) => {
let this_fp = CertFingerprint::new_raw(cert);
if this_fp == tlsa_fp {
return Ok(CertFingerprint::new_sha256(cert));
}
}
}
}
if let Some(sha256) = sha256 {
Ok(sha256)
} else if let Some(sha512) = sha512 {
Ok(sha512)
} else {
Ok(CertFingerprint::new_sha256(cert))
}
}
}
@ -97,49 +201,35 @@ impl SelfsignedCertVerifier for FileBasedCertVerifier {
_now: UnixTime,
) -> Result<bool, tokio_rustls::rustls::Error> {
//
// TODO: remove eprintln!()s and do overall code cleanup
// TODO: remove eprintln!()s
//
if let Some(known_cert) = self.map.get(host) {
// if host is found in known_hosts, compare certs
let this_fp = match known_cert.fingerprint {
let this_hash = match known_cert.fingerprint {
CertFingerprint::Sha256(_) => CertFingerprint::new_sha256(cert),
CertFingerprint::Sha512(_) => CertFingerprint::new_sha512(cert),
_ => unreachable!(),
CertFingerprint::Raw(_) => CertFingerprint::new_raw(cert),
};
Ok(this_fp == known_cert.fingerprint)
Ok(this_hash == known_cert.fingerprint)
} else {
// host is unknown, generate hash and add to known_hosts
#[cfg(feature = "hickory")]
let this_hash = match self.dane(cert, host, 1965) {
Ok(hash) => hash,
Err(e) => {
eprintln!("DANE verification failed: {:?}", e);
CertFingerprint::new_sha256(cert)
}
};
#[cfg(not(feature = "hickory"))]
let this_hash = CertFingerprint::new_sha256(cert);
let this_fp = this_hash.base64();
// TODO: DANE cert check, use this_hash.hex() for this
eprintln!(
"Warning: updating known_hosts with cert {} for {}",
&this_fp, &host,
);
(|| {
// trick with cloning file descriptor
// because we are not allowed to mutate &self
let mut f = std::fs::File::from(self.fd.lock().unwrap().try_clone()?);
f.write_all(host.as_bytes())?;
f.write_all(b" 0 sha256 ")?; // TODO after implementing `expires`
f.write_all(this_fp.as_bytes())?;
f.write_all(b"\n")?;
Ok::<(), std::io::Error>(())
})()
.unwrap_or_else(|e| {
eprintln!("Could not add cert to file: {:?}", e);
self.add_trusted_cert(host, this_hash).unwrap_or_else(|e| {
eprintln!("Unable to add new cert: {:?}", e);
});
self.map.insert(
host.to_owned(),
SelfsignedCert {
fingerprint: this_hash,
expires: 0, // TODO after implementing cert parsing in tokio-gemini
},
);
Ok(true)
}
}

View file

@ -125,3 +125,13 @@ impl CertFingerprint {
}
}
}
impl CertFingerprint {
pub fn fingerprint_type_str(&self) -> &'static str {
match self {
Self::Sha256(_) => "sha256",
Self::Sha512(_) => "sha512",
Self::Raw(_) => "raw",
}
}
}

View file

@ -4,6 +4,8 @@
use hickory_client::{
error::ClientError as HickoryClientError, proto::error::ProtoError as HickoryProtoError,
};
#[cfg(feature = "hickory")]
use tokio::runtime::TryCurrentError;
/// Main error structure, also a wrapper for everything else
#[derive(Debug)]
@ -29,6 +31,10 @@ pub enum LibError {
/// Hickory Proto error
#[cfg(feature = "hickory")]
DnsProtoError(HickoryProtoError),
/// Could not get Tokio runtime handle
/// inside Rustls cert verifier
#[cfg(feature = "hickory")]
NoTokioRuntime(TryCurrentError),
}
impl From<std::io::Error> for LibError {
@ -89,6 +95,14 @@ impl From<HickoryProtoError> for LibError {
}
}
#[cfg(feature = "hickory")]
impl From<TryCurrentError> for LibError {
#[inline]
fn from(err: TryCurrentError) -> Self {
Self::NoTokioRuntime(err)
}
}
/// URL parse or check error
#[derive(Debug)]
pub enum InvalidUrl {