From 17ba4060b8049c401941377a45d2a55fd8c2be24 Mon Sep 17 00:00:00 2001 From: DarkCat09 Date: Tue, 6 Aug 2024 21:03:29 +0400 Subject: [PATCH] feat: add file-based ss cert verifier (move from main example), v0.4.0 --- Cargo.lock | 2 +- Cargo.toml | 8 ++- examples/main.rs | 152 +---------------------------------------- src/certs/file_sscv.rs | 151 ++++++++++++++++++++++++++++++++++++++++ src/certs/mod.rs | 3 + 5 files changed, 164 insertions(+), 152 deletions(-) create mode 100644 src/certs/file_sscv.rs diff --git a/Cargo.lock b/Cargo.lock index 167a9dd..975411f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,7 +487,7 @@ dependencies = [ [[package]] name = "tokio-gemini" -version = "0.3.0" +version = "0.4.0" dependencies = [ "base64ct", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 79a7201..4f8c96b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokio-gemini" -version = "0.3.0" +version = "0.4.0" edition = "2021" license = "Apache-2.0" homepage = "https://unw.dc09.ru" @@ -12,6 +12,7 @@ categories = ["network-programming"] [dependencies] base64ct = "1.6.0" bytes = "1.7.1" +dashmap = { version = "6.0.1", optional = true } mime = "0.3.17" num_enum = "0.7.3" sha2 = "0.10.8" @@ -27,7 +28,10 @@ path = "examples/simple.rs" [[example]] name = "main" path = "examples/main.rs" +required-features = ["file-sscv"] [dev-dependencies] -dashmap = "6.0.1" tokio = { version = "1.39.2", features = ["macros", "rt-multi-thread", "io-util", "fs"] } + +[features] +file-sscv = ["dep:dashmap"] diff --git a/examples/main.rs b/examples/main.rs index 6598b76..db4939a 100644 --- a/examples/main.rs +++ b/examples/main.rs @@ -1,17 +1,8 @@ -use std::{io::Write, os::fd::AsFd, sync::Mutex}; - -use dashmap::DashMap; -use tokio::io::AsyncBufReadExt; -use tokio_gemini::certs::{ - fingerprint::{self, generate_fingerprint}, - insecure::AllowAllCertVerifier, - SelfsignedCert, SelfsignedCertVerifier, -}; +use tokio_gemini::certs::{file_sscv::FileBasedCertVerifier, insecure::AllowAllCertVerifier}; // -// cargo add tokio_gemini +// cargo add tokio_gemini -F file-sscv // cargo add tokio -F macros,rt-multi-thread,io-util,fs -// cargo add dashmap // #[tokio::main] @@ -37,7 +28,7 @@ async fn main() -> Result<(), tokio_gemini::LibError> { .build() } else { tokio_gemini::Client::builder() - .with_selfsigned_cert_verifier(CertVerifier::init().await?) + .with_selfsigned_cert_verifier(FileBasedCertVerifier::init("known_hosts").await?) .build() }; @@ -66,140 +57,3 @@ async fn main() -> Result<(), tokio_gemini::LibError> { Ok(()) } - -struct CertVerifier { - f: Mutex, - map: DashMap, -} - -impl CertVerifier { - async fn init() -> Result { - let map = DashMap::new(); - - if tokio::fs::try_exists("known_hosts").await? { - let mut f = tokio::fs::OpenOptions::new() - .read(true) - .open("known_hosts") - .await?; - - let mut reader = tokio::io::BufReader::new(&mut f); - - let mut buf = String::new(); - loop { - buf.clear(); - let n = reader.read_line(&mut buf).await?; - if n == 0 { - break; - } - - // Format: - // host expires hash-algo fingerprint - // Example: - // dc09.ru 1722930541 sha512 dGVzdHRlc3R0ZXN0Cg - if let [host, expires, algo, fp] = buf - .split_whitespace() - .take(4) - .collect::>() - .as_slice() - { - let expires = if let Ok(num) = expires.parse::() { - num - } else { - eprintln!("Cannot parse expires = {:?} as u64", expires); - continue; - }; - - let algo = match algo { - &"sha256" => fingerprint::Algorithm::Sha256, - &"sha512" => fingerprint::Algorithm::Sha512, - &_ => { - eprintln!("Unknown hash algorithm {:?}, skipping", algo); - continue; - } - }; - - map.insert( - (*host).to_owned(), - SelfsignedCert { - algo, - fingerprint: (*fp).to_owned(), - expires, - }, - ); - } else { - eprintln!("Cannot parse line: {:?}", buf); - continue; - } - } - } - - let f = Mutex::new( - std::fs::OpenOptions::new() - .append(true) - .create(true) - .open("known_hosts")?, - ); - - Ok(CertVerifier { f, map }) - } -} - -impl SelfsignedCertVerifier for CertVerifier { - fn verify( - &self, - cert: &tokio_gemini::certs::CertificateDer, - host: &str, - _now: tokio_gemini::certs::UnixTime, - ) -> Result { - if let Some(known_cert) = self.map.get(host) { - // if host is found in known_hosts, compare certs - let this_fp = generate_fingerprint(cert, known_cert.algo); - if this_fp == known_cert.fingerprint { - // current cert hash matches known cert hash - eprintln!("Cert for {} matched: {}", &host, &this_fp); - Ok(true) - } else { - // TODO (after implementing `expires`) update cert if known is expired - eprintln!( - "Error: certs do not match! Possibly MitM attack.\nKnown FP: {}\nGot: {}", - &known_cert.fingerprint, &this_fp, - ); - Ok(false) - } - } else { - // host is unknown, generate hash and add to known_hosts - let this_fp = generate_fingerprint(cert, fingerprint::Algorithm::Sha512); - 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.f.lock().unwrap().as_fd().try_clone_to_owned().unwrap(), - ); - f.write_all(host.as_bytes())?; - f.write_all(b" 0 sha512 ")?; // 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.map.insert( - host.to_owned(), - SelfsignedCert { - algo: fingerprint::Algorithm::Sha512, - fingerprint: this_fp, - expires: 0, // TODO after implementing cert parsing in tokio-gemini - }, - ); - - Ok(true) - } - } -} diff --git a/src/certs/file_sscv.rs b/src/certs/file_sscv.rs new file mode 100644 index 0000000..9fba98b --- /dev/null +++ b/src/certs/file_sscv.rs @@ -0,0 +1,151 @@ +use std::{io::Write, os::fd::AsFd, sync::Mutex}; + +use dashmap::DashMap; +use tokio::io::AsyncBufReadExt; +use tokio_rustls::rustls::pki_types::{CertificateDer, UnixTime}; + +use crate::{ + certs::{ + fingerprint::{generate_fingerprint, Algorithm}, + SelfsignedCert, SelfsignedCertVerifier, + }, + LibError, +}; + +pub struct FileBasedCertVerifier { + fd: Mutex, + map: DashMap, +} + +impl FileBasedCertVerifier { + pub async fn init(path: &str) -> Result { + let map = DashMap::new(); + + if tokio::fs::try_exists(path).await? { + let mut f = tokio::fs::OpenOptions::new().read(true).open(path).await?; + + let mut reader = tokio::io::BufReader::new(&mut f); + + let mut buf = String::new(); + loop { + buf.clear(); + let n = reader.read_line(&mut buf).await?; + if n == 0 { + break; + } + + // Format: + // host expires hash-algo fingerprint + // Example: + // dc09.ru 1722930541 sha512 dGVzdHRlc3R0ZXN0Cg + if let [host, expires, algo, fp] = buf + .split_whitespace() + .take(4) + .collect::>() + .as_slice() + { + let expires = if let Ok(num) = expires.parse::() { + num + } else { + eprintln!("Cannot parse expires = {:?} as u64", expires); + continue; + }; + + let algo = match *algo { + "sha256" => Algorithm::Sha256, + "sha512" => Algorithm::Sha512, + _ => { + eprintln!("Unknown hash algorithm {:?}, skipping", algo); + continue; + } + }; + + map.insert( + (*host).to_owned(), + SelfsignedCert { + algo, + fingerprint: (*fp).to_owned(), + expires, + }, + ); + } else { + eprintln!("Cannot parse line: {:?}", buf); + continue; + } + } + } + + let fd = Mutex::new( + std::fs::OpenOptions::new() + .append(true) + .create(true) + .open(path)? + .as_fd() + .try_clone_to_owned()?, + ); + + Ok(FileBasedCertVerifier { fd, map }) + } +} + +impl SelfsignedCertVerifier for FileBasedCertVerifier { + fn verify( + &self, + cert: &CertificateDer, + host: &str, + _now: UnixTime, + ) -> Result { + // + // TODO: remove eprintln!()s and do overall code cleanup + // + + if let Some(known_cert) = self.map.get(host) { + // if host is found in known_hosts, compare certs + let this_fp = generate_fingerprint(cert, known_cert.algo); + if this_fp == known_cert.fingerprint { + // current cert hash matches known cert hash + eprintln!("Cert for {} matched: {}", &host, &this_fp); + Ok(true) + } else { + // TODO (after implementing `expires`) update cert if known is expired + eprintln!( + "Error: certs do not match! Possibly MitM attack.\nKnown FP: {}\nGot: {}", + &known_cert.fingerprint, &this_fp, + ); + Ok(false) + } + } else { + // host is unknown, generate hash and add to known_hosts + let this_fp = generate_fingerprint(cert, Algorithm::Sha512); + 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 sha512 ")?; // 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.map.insert( + host.to_owned(), + SelfsignedCert { + algo: Algorithm::Sha512, + fingerprint: this_fp, + expires: 0, // TODO after implementing cert parsing in tokio-gemini + }, + ); + + Ok(true) + } + } +} diff --git a/src/certs/mod.rs b/src/certs/mod.rs index e36b6c5..7931c3c 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -1,6 +1,9 @@ pub mod fingerprint; pub mod insecure; +#[cfg(feature = "file-sscv")] +pub mod file_sscv; + pub(crate) mod verifier; pub use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};