Compare commits

..

No commits in common. "5533ec0d2bedea7eb6a6a8057fd9902f0c795628" and "dfeaa5044050c22ef567e1f3583a1ff3debc6ca9" have entirely different histories.

7 changed files with 73 additions and 245 deletions

1
.gitignore vendored
View file

@ -1,2 +1 @@
/target /target
known_hosts

77
Cargo.lock generated
View file

@ -17,12 +17,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "autocfg"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.73" version = "0.3.73"
@ -44,12 +38,6 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -86,12 +74,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@ -102,20 +84,6 @@ dependencies = [
"typenum", "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]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -206,16 +174,6 @@ version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 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]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.4"
@ -285,19 +243,6 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@ -337,15 +282,6 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "ring" name = "ring"
version = "0.17.8" version = "0.17.8"
@ -398,12 +334,6 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.8" version = "0.10.8"
@ -415,12 +345,6 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.7" version = "0.5.7"
@ -491,7 +415,6 @@ version = "0.2.0"
dependencies = [ dependencies = [
"base64ct", "base64ct",
"bytes", "bytes",
"dashmap",
"mime", "mime",
"num_enum", "num_enum",
"sha2", "sha2",

View file

@ -25,5 +25,4 @@ name = "main"
path = "examples/main.rs" path = "examples/main.rs"
[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"] }

View file

@ -1,11 +1,7 @@
use std::{io::Write, os::fd::AsFd, sync::Mutex}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio_rustls::rustls::{
use dashmap::DashMap; client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
use tokio::io::AsyncBufReadExt; ClientConfig, SignatureScheme,
use tokio_gemini::certs::{
fingerprint::{self, generate_fingerprint},
insecure::AllowAllCertVerifier,
verifier::{SelfsignedCert, SelfsignedCertVerifier},
}; };
#[tokio::main] #[tokio::main]
@ -26,175 +22,91 @@ async fn main() -> Result<(), tokio_gemini::LibError> {
} }
let client = if insecure { let client = if insecure {
tokio_gemini::Client::builder() tokio_gemini::Client::from(get_insecure_config())
.with_custom_verifier(AllowAllCertVerifier::yes_i_know_what_i_am_doing())
.build()
} else { } else {
tokio_gemini::Client::builder() tokio_gemini::Client::default()
.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 mut resp = client.request(&url).await?;
{ {
let status_code = resp.status().status_code(); let status_code = resp.status().status_code();
let status_num: u8 = status_code.into(); let status_num: u8 = status_code.into();
eprintln!("{} {:?}", status_num, status_code); eprintln!("{} {:?}", status_num, status_code);
} }
if resp.status().reply_type() == tokio_gemini::ReplyType::Success { if resp.status().reply_type() == tokio_gemini::ReplyType::Success {
let mime = resp.mime()?; let mime = resp.mime()?;
eprintln!("Mime: {}", mime); eprintln!("Mime: {}", mime);
let mut buf = [0u8, 128];
let body = resp.body();
if mime.type_() == mime::TEXT { if mime.type_() == mime::TEXT {
println!("{}", resp.text().await?); loop {
let n = body.read(&mut buf).await?;
if n == 0 {
break;
}
print!("{}", std::str::from_utf8(&buf[..n])?);
}
} else { } else {
eprintln!("Downloading into content.bin"); eprintln!("Downloading into content.bin");
let mut f = tokio::fs::File::create("content.bin").await?; let mut f = tokio::fs::File::create("content.bin").await?;
tokio::io::copy(&mut resp.stream(), &mut f).await?; loop {
let n = body.read(&mut buf).await?;
if n == 0 {
break;
}
f.write_all(&buf[..n]).await?;
}
} }
} else { } else {
eprintln!("Message: {}", resp.message()); eprintln!("Message: {}", resp.message());
} }
Ok(()) Ok(())
} }
struct CertVerifier { fn get_insecure_config() -> ClientConfig {
f: Mutex<std::fs::File>, ClientConfig::builder()
map: DashMap<String, SelfsignedCert>, .dangerous()
.with_custom_certificate_verifier(std::sync::Arc::new(NoCertVerification {}))
.with_no_client_auth()
} }
impl CertVerifier { #[derive(Debug)]
async fn init() -> Result<Self, tokio_gemini::LibError> { struct NoCertVerification;
let map = DashMap::new();
if tokio::fs::try_exists("known_hosts").await? { impl ServerCertVerifier for NoCertVerification {
let mut f = tokio::fs::OpenOptions::new() fn verify_server_cert(
.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, &self,
cert: &tokio_gemini::certs::verifier::CertificateDer, end_entity: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
host: &str, intermediates: &[tokio_rustls::rustls::pki_types::CertificateDer<'_>],
_now: tokio_gemini::certs::verifier::UnixTime, server_name: &tokio_rustls::rustls::pki_types::ServerName<'_>,
) -> Result<bool, tokio_rustls::rustls::Error> { ocsp_response: &[u8],
if let Some(known_cert) = self.map.get(host) { now: tokio_rustls::rustls::pki_types::UnixTime,
// if host is found in known_hosts, compare certs ) -> Result<ServerCertVerified, tokio_rustls::rustls::Error> {
let this_fp = generate_fingerprint(cert, known_cert.algo); Ok(ServerCertVerified::assertion())
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,
);
(|| { fn verify_tls12_signature(
// trick with cloning file descriptor &self,
// because we are not allowed to mutate &self message: &[u8],
let mut f = std::fs::File::from( cert: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
self.f.lock().unwrap().as_fd().try_clone_to_owned().unwrap(), dss: &tokio_rustls::rustls::DigitallySignedStruct,
); ) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
f.write_all(host.as_bytes())?; Ok(HandshakeSignatureValid::assertion())
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)
} }
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
dss: &tokio_rustls::rustls::DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<tokio_rustls::rustls::SignatureScheme> {
vec![
SignatureScheme::ECDSA_NISTP256_SHA256,
SignatureScheme::ECDSA_NISTP384_SHA384,
SignatureScheme::ECDSA_NISTP521_SHA512,
]
} }
} }

View file

@ -6,13 +6,15 @@ use super::verifier::CertificateDer;
const SHA256_B64_LEN: usize = 44; // 4 * ((256 / 8) as f64 / 3 as f64).ceil() const SHA256_B64_LEN: usize = 44; // 4 * ((256 / 8) as f64 / 3 as f64).ceil()
const SHA512_B64_LEN: usize = 88; // 4 * ((512 / 8) as f64 / 3 as f64).ceil() const SHA512_B64_LEN: usize = 88; // 4 * ((512 / 8) as f64 / 3 as f64).ceil()
#[derive(Debug, Clone, Copy)]
pub enum Algorithm { pub enum Algorithm {
Sha256, Sha256,
Sha512, Sha512,
} }
pub fn generate_fingerprint(cert: &CertificateDer, algo: Algorithm) -> String { pub fn generate_fingerprint(
cert: &CertificateDer,
algo: Algorithm,
) -> Result<String, base64ct::InvalidLengthError> {
match algo { match algo {
Algorithm::Sha256 => { Algorithm::Sha256 => {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
@ -21,12 +23,7 @@ pub fn generate_fingerprint(cert: &CertificateDer, algo: Algorithm) -> String {
} }
let bin = hasher.finalize(); let bin = hasher.finalize();
let mut buf = [0; SHA256_B64_LEN]; let mut buf = [0; SHA256_B64_LEN];
// Note on unwrap: Base64Unpadded::encode(&bin, &mut buf).map(|hash| hash.to_owned())
// Encoder returns error only if buffer length is insufficient.
// SHA-256 is *always* 256 bits (32 bytes),
// after we apply base64 formula we get 44 bytes in output including padding.
// See also comment near const SHA256_B64_LEN
Base64Unpadded::encode(&bin, &mut buf).unwrap().to_owned()
} }
Algorithm::Sha512 => { Algorithm::Sha512 => {
let mut hasher = Sha512::new(); let mut hasher = Sha512::new();
@ -35,9 +32,7 @@ pub fn generate_fingerprint(cert: &CertificateDer, algo: Algorithm) -> String {
} }
let bin = hasher.finalize(); let bin = hasher.finalize();
let mut buf = [0; SHA512_B64_LEN]; let mut buf = [0; SHA512_B64_LEN];
// Same explanation for unwrap, see above Base64Unpadded::encode(&bin, &mut buf).map(|hash| hash.to_owned())
// SHA-512 is always 512 bits or 64 bytes
Base64Unpadded::encode(&bin, &mut buf).unwrap().to_owned()
} }
} }
} }

View file

@ -5,11 +5,11 @@ use tokio_rustls::rustls::{
}; };
#[derive(Debug)] #[derive(Debug)]
pub struct AllowAllCertVerifier(std::sync::Arc<CryptoProvider>); pub struct AllowAllCertVerifier(CryptoProvider);
impl AllowAllCertVerifier { impl AllowAllCertVerifier {
pub fn yes_i_know_what_i_am_doing() -> Self { fn yes_i_know_what_i_am_doing(provider: CryptoProvider) -> Self {
AllowAllCertVerifier(CryptoProvider::get_default().unwrap().clone()) AllowAllCertVerifier(provider)
} }
} }

View file

@ -17,9 +17,9 @@ pub trait SelfsignedCertVerifier: Send + Sync {
} }
pub struct SelfsignedCert { pub struct SelfsignedCert {
pub algo: super::fingerprint::Algorithm, algo: super::fingerprint::Algorithm,
pub fingerprint: String, fingerprint: String,
pub expires: u64, expires: u64,
} }
pub struct CustomCertVerifier { pub struct CustomCertVerifier {