diff --git a/.gitignore b/.gitignore index ea8c4bf..2107883 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +known_hosts diff --git a/Cargo.lock b/Cargo.lock index 903c1c7..2197218 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + [[package]] name = "backtrace" version = "0.3.73" @@ -38,6 +44,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "block-buffer" version = "0.10.4" @@ -74,6 +86,12 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "crypto-common" version = "0.1.6" @@ -84,6 +102,20 @@ dependencies = [ "typenum", ] +[[package]] +name = "dashmap" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "digest" version = "0.10.7" @@ -174,6 +206,16 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "memchr" version = "2.7.4" @@ -243,6 +285,19 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -282,6 +337,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags", +] + [[package]] name = "ring" version = "0.17.8" @@ -334,6 +398,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sha2" version = "0.10.8" @@ -345,6 +415,12 @@ dependencies = [ "digest", ] +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + [[package]] name = "socket2" version = "0.5.7" @@ -415,6 +491,7 @@ version = "0.2.0" dependencies = [ "base64ct", "bytes", + "dashmap", "mime", "num_enum", "sha2", diff --git a/Cargo.toml b/Cargo.toml index a4f685b..12f6b06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,4 +25,5 @@ name = "main" path = "examples/main.rs" [dev-dependencies] +dashmap = "6.0.1" tokio = { version = "1.39.2", features = ["macros", "rt-multi-thread", "io-util", "fs"] } diff --git a/examples/main.rs b/examples/main.rs index 81426ac..7bcd214 100644 --- a/examples/main.rs +++ b/examples/main.rs @@ -1,7 +1,11 @@ -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio_rustls::rustls::{ - client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, - ClientConfig, SignatureScheme, +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, + verifier::{SelfsignedCert, SelfsignedCertVerifier}, }; #[tokio::main] @@ -22,91 +26,175 @@ async fn main() -> Result<(), tokio_gemini::LibError> { } let client = if insecure { - tokio_gemini::Client::from(get_insecure_config()) + tokio_gemini::Client::builder() + .with_custom_verifier(AllowAllCertVerifier::yes_i_know_what_i_am_doing()) + .build() } else { - tokio_gemini::Client::default() + tokio_gemini::Client::builder() + .with_webpki_roots() // TODO: do we really need them? + .with_selfsigned_cert_verifier(CertVerifier::init().await?) + .build() }; + let mut resp = client.request(&url).await?; + { let status_code = resp.status().status_code(); let status_num: u8 = status_code.into(); eprintln!("{} {:?}", status_num, status_code); } + if resp.status().reply_type() == tokio_gemini::ReplyType::Success { let mime = resp.mime()?; eprintln!("Mime: {}", mime); - let mut buf = [0u8, 128]; - let body = resp.body(); + if mime.type_() == mime::TEXT { - loop { - let n = body.read(&mut buf).await?; - if n == 0 { - break; - } - print!("{}", std::str::from_utf8(&buf[..n])?); - } + println!("{}", resp.text().await?); } else { eprintln!("Downloading into content.bin"); let mut f = tokio::fs::File::create("content.bin").await?; - loop { - let n = body.read(&mut buf).await?; - if n == 0 { - break; - } - f.write_all(&buf[..n]).await?; - } + tokio::io::copy(&mut resp.stream(), &mut f).await?; } } else { eprintln!("Message: {}", resp.message()); } + Ok(()) } -fn get_insecure_config() -> ClientConfig { - ClientConfig::builder() - .dangerous() - .with_custom_certificate_verifier(std::sync::Arc::new(NoCertVerification {})) - .with_no_client_auth() +struct CertVerifier { + f: Mutex, + map: DashMap, } -#[derive(Debug)] -struct NoCertVerification; +impl CertVerifier { + async fn init() -> Result { + let map = DashMap::new(); -impl ServerCertVerifier for NoCertVerification { - fn verify_server_cert( - &self, - end_entity: &tokio_rustls::rustls::pki_types::CertificateDer<'_>, - intermediates: &[tokio_rustls::rustls::pki_types::CertificateDer<'_>], - server_name: &tokio_rustls::rustls::pki_types::ServerName<'_>, - ocsp_response: &[u8], - now: tokio_rustls::rustls::pki_types::UnixTime, - ) -> Result { - Ok(ServerCertVerified::assertion()) - } + if tokio::fs::try_exists("known_hosts").await? { + let mut f = tokio::fs::OpenOptions::new() + .read(true) + .open("known_hosts") + .await?; - fn verify_tls12_signature( - &self, - message: &[u8], - cert: &tokio_rustls::rustls::pki_types::CertificateDer<'_>, - dss: &tokio_rustls::rustls::DigitallySignedStruct, - ) -> Result { - Ok(HandshakeSignatureValid::assertion()) - } + let mut reader = tokio::io::BufReader::new(&mut f); - fn verify_tls13_signature( - &self, - message: &[u8], - cert: &tokio_rustls::rustls::pki_types::CertificateDer<'_>, - dss: &tokio_rustls::rustls::DigitallySignedStruct, - ) -> Result { - Ok(HandshakeSignatureValid::assertion()) - } + let mut buf = String::new(); + loop { + buf.clear(); + let n = reader.read_line(&mut buf).await?; + if n == 0 { + break; + } - fn supported_verify_schemes(&self) -> Vec { - vec![ - SignatureScheme::ECDSA_NISTP256_SHA256, - SignatureScheme::ECDSA_NISTP384_SHA384, - SignatureScheme::ECDSA_NISTP521_SHA512, - ] + // 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::verifier::CertificateDer, + host: &str, + _now: tokio_gemini::certs::verifier::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(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) + } } }