From 8218fd584e4053848738a310258893e14c4ee10e Mon Sep 17 00:00:00 2001 From: salmonik Date: Mon, 24 Aug 2026 15:27:06 +0200 Subject: TOFU host trust: SPKI fingerprint pinning, tests, forget-host --- src/bin/client/connection.rs | 174 +++++++++++++++++++++---------------------- 1 file changed, 87 insertions(+), 87 deletions(-) (limited to 'src/bin/client/connection.rs') diff --git a/src/bin/client/connection.rs b/src/bin/client/connection.rs index c5d506d..6aff321 100644 --- a/src/bin/client/connection.rs +++ b/src/bin/client/connection.rs @@ -1,144 +1,144 @@ use crate::database; -use native_tls::TlsConnector; +use crate::tls::TlsConnector; use rusqlite::Connection; use std::io::{Read, Write}; use std::net::TcpStream; use std::sync::mpsc; -use std::sync::Arc; -use std::sync::Mutex; +use parking_lot::Mutex; use std::thread; use std::time::{Duration, Instant}; static UNSAFE_MODE: Mutex = Mutex::new(false); pub fn set_unsafe_mode(unsafe_mode: bool) { - let mut mode = UNSAFE_MODE.lock().unwrap(); - *mode = unsafe_mode; + *UNSAFE_MODE.lock() = unsafe_mode; } +type ConnectResult = Result<(TlsStream, Option), Box>; + pub fn connect( addr: &str, conn: &Connection, ) -> Result<(TlsStream, Option), Box> { - let (tx, rx) = mpsc::channel(); + let stored_fingerprint = database::get_fingerprint(conn, addr); + let unsafe_mode = *UNSAFE_MODE.lock(); + + let (tx, rx) = mpsc::channel::(); let addr_str = addr.to_string(); let server_name = addr_str .split(':') .next() .unwrap_or("127.0.0.1") .to_string(); - thread::spawn(move || match TcpStream::connect(&addr_str) { - Ok(s) => tx.send(Ok(s)).unwrap(), - Err(e) => tx.send(Err(e)).unwrap(), + + thread::spawn(move || { + let outcome = (|| -> ConnectResult { + let tcp = TcpStream::connect(&addr_str)?; + let connector = TlsConnector::new(&server_name, stored_fingerprint.clone(), unsafe_mode)?; + let tls_stream = connector.connect(tcp)?; + let fp = connector.observed_fingerprint(); + Ok((TlsStream { inner: tls_stream }, fp)) + })(); + let _ = tx.send(outcome); }); - match rx.recv_timeout(Duration::from_secs(3)) { - Ok(Ok(s)) => { - let unsafe_mode = *UNSAFE_MODE.lock().unwrap(); - - let connector = if unsafe_mode { - TlsConnector::builder() - .danger_accept_invalid_certs(true) - .danger_accept_invalid_hostnames(true) - .build()? - } else { - TlsConnector::builder().build()? - }; - - let tls_stream = connector.connect(&server_name, s)?; - let mut tls = TlsStream::new(tls_stream); - - let start = Instant::now(); - tls.write_line("PING")?; - let mut response = String::new(); - tls.read_line(&mut response)?; - let elapsed = start.elapsed(); - println!( - "\n\rConnected to {} with ping delay: {:?} (TLS)", - addr, elapsed - ); - - if let Some((username, password)) = database::get_credentials(conn, addr) { - let auth_msg = format!("AUTH {} {}", username, password); - tls.write_line(&auth_msg)?; - let mut auth_response = String::new(); - tls.read_line(&mut auth_response)?; - if auth_response.trim().starts_with("OK") { - println!("Authenticated as {}", username); - Ok((tls, Some(username))) - } else { - println!("Authentication failed: {}", auth_response.trim()); - Ok((tls, None)) - } - } else { - Ok((tls, None)) + + // Channel carries (stream, observed fingerprint). + let (tls, observed) = match rx.recv_timeout(Duration::from_secs(3)) { + Ok(Ok((tls, fp))) => (tls, fp), + Ok(Err(e)) => return Err(Box::new(TrustError(format!("{e}")))), + Err(mpsc::RecvTimeoutError::Timeout) => { + return Err("Connection timed out after 3 seconds".into()) + } + Err(_) => return Err("Connection attempt failed".into()), + }; + + // TOFU: persist the fingerprint on first contact. + if !unsafe_mode { + if let Some(fp) = observed { + if database::get_fingerprint(conn, addr).is_none() { + database::save_fingerprint(conn, addr, &fp)?; + println!( + "First contact with {addr}: trusting fingerprint {fp}\n\ + Future connections must present the same certificate key." + ); } } - Ok(Err(e)) => Err(Box::new(e)), - Err(mpsc::RecvTimeoutError::Timeout) => Err("Connection timed out after 3 seconds".into()), - Err(_) => Err("Connection attempt failed".into()), } -} -pub struct TlsStream { - stream: Arc>>, + let mut tls = tls; + + let start = Instant::now(); + tls.write_line("PING")?; + let mut response = String::new(); + tls.read_line(&mut response)?; + let elapsed = start.elapsed(); + println!("\n\rConnected to {} with ping delay: {:?} (TLS)", addr, elapsed); + + if let Some((username, password)) = database::get_credentials(conn, addr) { + let auth_msg = format!("AUTH {} {}", username, password); + tls.write_line(&auth_msg)?; + let mut auth_response = String::new(); + tls.read_line(&mut auth_response)?; + if auth_response.trim().starts_with("OK") { + println!("Authenticated as {}", username); + Ok((tls, Some(username))) + } else { + println!("Authentication failed: {}", auth_response.trim()); + Ok((tls, None)) + } + } else { + Ok((tls, None)) + } } -unsafe impl Send for TlsStream {} -unsafe impl Sync for TlsStream {} +/// Wrapper so TLS handshake failures surface as errors with their message +/// intact (rustls errors already carry the mismatch details). +struct TrustError(String); -impl TlsStream { - pub fn new(stream: native_tls::TlsStream) -> Self { - Self { - stream: Arc::new(Mutex::new(stream)), - } +impl std::fmt::Display for TrustError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) } +} +impl std::fmt::Debug for TrustError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self}") + } +} +impl std::error::Error for TrustError {} + +pub struct TlsStream { + inner: crate::tls::TlsStream, +} +impl TlsStream { pub fn read_line(&mut self, buf: &mut String) -> std::io::Result { - let mut byte_buf = [0u8; 1024]; - let mut stream = self.stream.lock().unwrap(); - loop { - let n = stream.read(&mut byte_buf)?; - if n == 0 { - return Ok(0); - } - let chunk = std::str::from_utf8(&byte_buf[..n]).unwrap_or(""); - buf.push_str(chunk); - if buf.contains('\n') { - return Ok(n); - } - } + self.inner.read_line(buf) } pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> { - let mut stream = self.stream.lock().unwrap(); - stream.write_all(msg.as_bytes())?; - stream.write_all(b"\n")?; - stream.flush()?; - Ok(()) + self.inner.write_line(msg) } pub fn try_clone(&self) -> std::io::Result { Ok(TlsStream { - stream: Arc::clone(&self.stream), + inner: self.inner.try_clone()?, }) } } impl Read for TlsStream { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let mut stream = self.stream.lock().unwrap(); - stream.read(buf) + self.inner.read(buf) } } impl Write for TlsStream { fn write(&mut self, buf: &[u8]) -> std::io::Result { - let mut stream = self.stream.lock().unwrap(); - stream.write(buf) + self.inner.write(buf) } fn flush(&mut self) -> std::io::Result<()> { - let mut stream = self.stream.lock().unwrap(); - stream.flush() + self.inner.flush() } } -- cgit v1.2.3