feat: add file-based ss cert verifier (move from main example), v0.4.0
This commit is contained in:
parent
700981a5e3
commit
17ba4060b8
5 changed files with 164 additions and 152 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -487,7 +487,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tokio-gemini"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"bytes",
|
||||
|
|
|
@ -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"]
|
||||
|
|
152
examples/main.rs
152
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<std::fs::File>,
|
||||
map: DashMap<String, SelfsignedCert>,
|
||||
}
|
||||
|
||||
impl CertVerifier {
|
||||
async fn init() -> Result<Self, tokio_gemini::LibError> {
|
||||
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 <space> expires <space> hash-algo <space> fingerprint
|
||||
// Example:
|
||||
// dc09.ru 1722930541 sha512 dGVzdHRlc3R0ZXN0Cg
|
||||
if let [host, expires, algo, fp] = buf
|
||||
.split_whitespace()
|
||||
.take(4)
|
||||
.collect::<Vec<&str>>()
|
||||
.as_slice()
|
||||
{
|
||||
let expires = if let Ok(num) = expires.parse::<u64>() {
|
||||
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<bool, tokio_rustls::rustls::Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
151
src/certs/file_sscv.rs
Normal file
151
src/certs/file_sscv.rs
Normal file
|
@ -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<std::os::fd::OwnedFd>,
|
||||
map: DashMap<String, SelfsignedCert>,
|
||||
}
|
||||
|
||||
impl FileBasedCertVerifier {
|
||||
pub async fn init(path: &str) -> Result<Self, LibError> {
|
||||
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 <space> expires <space> hash-algo <space> fingerprint
|
||||
// Example:
|
||||
// dc09.ru 1722930541 sha512 dGVzdHRlc3R0ZXN0Cg
|
||||
if let [host, expires, algo, fp] = buf
|
||||
.split_whitespace()
|
||||
.take(4)
|
||||
.collect::<Vec<&str>>()
|
||||
.as_slice()
|
||||
{
|
||||
let expires = if let Ok(num) = expires.parse::<u64>() {
|
||||
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<bool, tokio_rustls::rustls::Error> {
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
|
|
Loading…
Reference in a new issue