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]]
|
[[package]]
|
||||||
name = "tokio-gemini"
|
name = "tokio-gemini"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64ct",
|
"base64ct",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tokio-gemini"
|
name = "tokio-gemini"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
homepage = "https://unw.dc09.ru"
|
homepage = "https://unw.dc09.ru"
|
||||||
|
@ -12,6 +12,7 @@ categories = ["network-programming"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64ct = "1.6.0"
|
base64ct = "1.6.0"
|
||||||
bytes = "1.7.1"
|
bytes = "1.7.1"
|
||||||
|
dashmap = { version = "6.0.1", optional = true }
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
num_enum = "0.7.3"
|
num_enum = "0.7.3"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
|
@ -27,7 +28,10 @@ path = "examples/simple.rs"
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "main"
|
name = "main"
|
||||||
path = "examples/main.rs"
|
path = "examples/main.rs"
|
||||||
|
required-features = ["file-sscv"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dashmap = "6.0.1"
|
|
||||||
tokio = { version = "1.39.2", features = ["macros", "rt-multi-thread", "io-util", "fs"] }
|
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 tokio_gemini::certs::{file_sscv::FileBasedCertVerifier, insecure::AllowAllCertVerifier};
|
||||||
|
|
||||||
use dashmap::DashMap;
|
|
||||||
use tokio::io::AsyncBufReadExt;
|
|
||||||
use tokio_gemini::certs::{
|
|
||||||
fingerprint::{self, generate_fingerprint},
|
|
||||||
insecure::AllowAllCertVerifier,
|
|
||||||
SelfsignedCert, SelfsignedCertVerifier,
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// cargo add tokio_gemini
|
// cargo add tokio_gemini -F file-sscv
|
||||||
// cargo add tokio -F macros,rt-multi-thread,io-util,fs
|
// cargo add tokio -F macros,rt-multi-thread,io-util,fs
|
||||||
// cargo add dashmap
|
|
||||||
//
|
//
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
@ -37,7 +28,7 @@ async fn main() -> Result<(), tokio_gemini::LibError> {
|
||||||
.build()
|
.build()
|
||||||
} else {
|
} else {
|
||||||
tokio_gemini::Client::builder()
|
tokio_gemini::Client::builder()
|
||||||
.with_selfsigned_cert_verifier(CertVerifier::init().await?)
|
.with_selfsigned_cert_verifier(FileBasedCertVerifier::init("known_hosts").await?)
|
||||||
.build()
|
.build()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -66,140 +57,3 @@ async fn main() -> Result<(), tokio_gemini::LibError> {
|
||||||
|
|
||||||
Ok(())
|
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 fingerprint;
|
||||||
pub mod insecure;
|
pub mod insecure;
|
||||||
|
|
||||||
|
#[cfg(feature = "file-sscv")]
|
||||||
|
pub mod file_sscv;
|
||||||
|
|
||||||
pub(crate) mod verifier;
|
pub(crate) mod verifier;
|
||||||
|
|
||||||
pub use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
pub use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||||
|
|
Loading…
Add table
Reference in a new issue