diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-24 15:27:06 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-24 15:27:06 +0200 |
| commit | 8218fd584e4053848738a310258893e14c4ee10e (patch) | |
| tree | 2c92997ba0b6aa260fe72c88190f5f8b590457cb /src/bin/client/connection.rs | |
| parent | TLS transport and games framework on client and server (diff) | |
| download | zagan-bet-8218fd584e4053848738a310258893e14c4ee10e.tar.gz zagan-bet-8218fd584e4053848738a310258893e14c4ee10e.zip | |
TOFU host trust: SPKI fingerprint pinning, tests, forget-host
Diffstat (limited to '')
| -rw-r--r-- | src/bin/client/connection.rs | 174 |
1 files changed, 87 insertions, 87 deletions
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 @@ | |||
| 1 | use crate::database; | 1 | use crate::database; |
| 2 | use native_tls::TlsConnector; | 2 | use crate::tls::TlsConnector; |
| 3 | use rusqlite::Connection; | 3 | use rusqlite::Connection; |
| 4 | use std::io::{Read, Write}; | 4 | use std::io::{Read, Write}; |
| 5 | use std::net::TcpStream; | 5 | use std::net::TcpStream; |
| 6 | use std::sync::mpsc; | 6 | use std::sync::mpsc; |
| 7 | use std::sync::Arc; | 7 | use parking_lot::Mutex; |
| 8 | use std::sync::Mutex; | ||
| 9 | use std::thread; | 8 | use std::thread; |
| 10 | use std::time::{Duration, Instant}; | 9 | use std::time::{Duration, Instant}; |
| 11 | 10 | ||
| 12 | static UNSAFE_MODE: Mutex<bool> = Mutex::new(false); | 11 | static UNSAFE_MODE: Mutex<bool> = Mutex::new(false); |
| 13 | 12 | ||
| 14 | pub fn set_unsafe_mode(unsafe_mode: bool) { | 13 | pub fn set_unsafe_mode(unsafe_mode: bool) { |
| 15 | let mut mode = UNSAFE_MODE.lock().unwrap(); | 14 | *UNSAFE_MODE.lock() = unsafe_mode; |
| 16 | *mode = unsafe_mode; | ||
| 17 | } | 15 | } |
| 18 | 16 | ||
| 17 | type ConnectResult = Result<(TlsStream, Option<String>), Box<dyn std::error::Error + Send + Sync>>; | ||
| 18 | |||
| 19 | pub fn connect( | 19 | pub fn connect( |
| 20 | addr: &str, | 20 | addr: &str, |
| 21 | conn: &Connection, | 21 | conn: &Connection, |
| 22 | ) -> Result<(TlsStream, Option<String>), Box<dyn std::error::Error>> { | 22 | ) -> Result<(TlsStream, Option<String>), Box<dyn std::error::Error>> { |
| 23 | let (tx, rx) = mpsc::channel(); | 23 | let stored_fingerprint = database::get_fingerprint(conn, addr); |
| 24 | let unsafe_mode = *UNSAFE_MODE.lock(); | ||
| 25 | |||
| 26 | let (tx, rx) = mpsc::channel::<ConnectResult>(); | ||
| 24 | let addr_str = addr.to_string(); | 27 | let addr_str = addr.to_string(); |
| 25 | let server_name = addr_str | 28 | let server_name = addr_str |
| 26 | .split(':') | 29 | .split(':') |
| 27 | .next() | 30 | .next() |
| 28 | .unwrap_or("127.0.0.1") | 31 | .unwrap_or("127.0.0.1") |
| 29 | .to_string(); | 32 | .to_string(); |
| 30 | thread::spawn(move || match TcpStream::connect(&addr_str) { | 33 | |
| 31 | Ok(s) => tx.send(Ok(s)).unwrap(), | 34 | thread::spawn(move || { |
| 32 | Err(e) => tx.send(Err(e)).unwrap(), | 35 | let outcome = (|| -> ConnectResult { |
| 36 | let tcp = TcpStream::connect(&addr_str)?; | ||
| 37 | let connector = TlsConnector::new(&server_name, stored_fingerprint.clone(), unsafe_mode)?; | ||
| 38 | let tls_stream = connector.connect(tcp)?; | ||
| 39 | let fp = connector.observed_fingerprint(); | ||
| 40 | Ok((TlsStream { inner: tls_stream }, fp)) | ||
| 41 | })(); | ||
| 42 | let _ = tx.send(outcome); | ||
| 33 | }); | 43 | }); |
| 34 | match rx.recv_timeout(Duration::from_secs(3)) { | 44 | |
| 35 | Ok(Ok(s)) => { | 45 | // Channel carries (stream, observed fingerprint). |
| 36 | let unsafe_mode = *UNSAFE_MODE.lock().unwrap(); | 46 | let (tls, observed) = match rx.recv_timeout(Duration::from_secs(3)) { |
| 37 | 47 | Ok(Ok((tls, fp))) => (tls, fp), | |
| 38 | let connector = if unsafe_mode { | 48 | Ok(Err(e)) => return Err(Box::new(TrustError(format!("{e}")))), |
| 39 | TlsConnector::builder() | 49 | Err(mpsc::RecvTimeoutError::Timeout) => { |
| 40 | .danger_accept_invalid_certs(true) | 50 | return Err("Connection timed out after 3 seconds".into()) |
| 41 | .danger_accept_invalid_hostnames(true) | 51 | } |
| 42 | .build()? | 52 | Err(_) => return Err("Connection attempt failed".into()), |
| 43 | } else { | 53 | }; |
| 44 | TlsConnector::builder().build()? | 54 | |
| 45 | }; | 55 | // TOFU: persist the fingerprint on first contact. |
| 46 | 56 | if !unsafe_mode { | |
| 47 | let tls_stream = connector.connect(&server_name, s)?; | 57 | if let Some(fp) = observed { |
| 48 | let mut tls = TlsStream::new(tls_stream); | 58 | if database::get_fingerprint(conn, addr).is_none() { |
| 49 | 59 | database::save_fingerprint(conn, addr, &fp)?; | |
| 50 | let start = Instant::now(); | 60 | println!( |
| 51 | tls.write_line("PING")?; | 61 | "First contact with {addr}: trusting fingerprint {fp}\n\ |
| 52 | let mut response = String::new(); | 62 | Future connections must present the same certificate key." |
| 53 | tls.read_line(&mut response)?; | 63 | ); |
| 54 | let elapsed = start.elapsed(); | ||
| 55 | println!( | ||
| 56 | "\n\rConnected to {} with ping delay: {:?} (TLS)", | ||
| 57 | addr, elapsed | ||
| 58 | ); | ||
| 59 | |||
| 60 | if let Some((username, password)) = database::get_credentials(conn, addr) { | ||
| 61 | let auth_msg = format!("AUTH {} {}", username, password); | ||
| 62 | tls.write_line(&auth_msg)?; | ||
| 63 | let mut auth_response = String::new(); | ||
| 64 | tls.read_line(&mut auth_response)?; | ||
| 65 | if auth_response.trim().starts_with("OK") { | ||
| 66 | println!("Authenticated as {}", username); | ||
| 67 | Ok((tls, Some(username))) | ||
| 68 | } else { | ||
| 69 | println!("Authentication failed: {}", auth_response.trim()); | ||
| 70 | Ok((tls, None)) | ||
| 71 | } | ||
| 72 | } else { | ||
| 73 | Ok((tls, None)) | ||
| 74 | } | 64 | } |
| 75 | } | 65 | } |
| 76 | Ok(Err(e)) => Err(Box::new(e)), | ||
| 77 | Err(mpsc::RecvTimeoutError::Timeout) => Err("Connection timed out after 3 seconds".into()), | ||
| 78 | Err(_) => Err("Connection attempt failed".into()), | ||
| 79 | } | 66 | } |
| 80 | } | ||
| 81 | 67 | ||
| 82 | pub struct TlsStream { | 68 | let mut tls = tls; |
| 83 | stream: Arc<Mutex<native_tls::TlsStream<TcpStream>>>, | 69 | |
| 70 | let start = Instant::now(); | ||
| 71 | tls.write_line("PING")?; | ||
| 72 | let mut response = String::new(); | ||
| 73 | tls.read_line(&mut response)?; | ||
| 74 | let elapsed = start.elapsed(); | ||
| 75 | println!("\n\rConnected to {} with ping delay: {:?} (TLS)", addr, elapsed); | ||
| 76 | |||
| 77 | if let Some((username, password)) = database::get_credentials(conn, addr) { | ||
| 78 | let auth_msg = format!("AUTH {} {}", username, password); | ||
| 79 | tls.write_line(&auth_msg)?; | ||
| 80 | let mut auth_response = String::new(); | ||
| 81 | tls.read_line(&mut auth_response)?; | ||
| 82 | if auth_response.trim().starts_with("OK") { | ||
| 83 | println!("Authenticated as {}", username); | ||
| 84 | Ok((tls, Some(username))) | ||
| 85 | } else { | ||
| 86 | println!("Authentication failed: {}", auth_response.trim()); | ||
| 87 | Ok((tls, None)) | ||
| 88 | } | ||
| 89 | } else { | ||
| 90 | Ok((tls, None)) | ||
| 91 | } | ||
| 84 | } | 92 | } |
| 85 | 93 | ||
| 86 | unsafe impl Send for TlsStream {} | 94 | /// Wrapper so TLS handshake failures surface as errors with their message |
| 87 | unsafe impl Sync for TlsStream {} | 95 | /// intact (rustls errors already carry the mismatch details). |
| 96 | struct TrustError(String); | ||
| 88 | 97 | ||
| 89 | impl TlsStream { | 98 | impl std::fmt::Display for TrustError { |
| 90 | pub fn new(stream: native_tls::TlsStream<TcpStream>) -> Self { | 99 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 91 | Self { | 100 | write!(f, "{}", self.0) |
| 92 | stream: Arc::new(Mutex::new(stream)), | ||
| 93 | } | ||
| 94 | } | 101 | } |
| 102 | } | ||
| 103 | impl std::fmt::Debug for TrustError { | ||
| 104 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 105 | write!(f, "{self}") | ||
| 106 | } | ||
| 107 | } | ||
| 108 | impl std::error::Error for TrustError {} | ||
| 109 | |||
| 110 | pub struct TlsStream { | ||
| 111 | inner: crate::tls::TlsStream, | ||
| 112 | } | ||
| 95 | 113 | ||
| 114 | impl TlsStream { | ||
| 96 | pub fn read_line(&mut self, buf: &mut String) -> std::io::Result<usize> { | 115 | pub fn read_line(&mut self, buf: &mut String) -> std::io::Result<usize> { |
| 97 | let mut byte_buf = [0u8; 1024]; | 116 | self.inner.read_line(buf) |
| 98 | let mut stream = self.stream.lock().unwrap(); | ||
| 99 | loop { | ||
| 100 | let n = stream.read(&mut byte_buf)?; | ||
| 101 | if n == 0 { | ||
| 102 | return Ok(0); | ||
| 103 | } | ||
| 104 | let chunk = std::str::from_utf8(&byte_buf[..n]).unwrap_or(""); | ||
| 105 | buf.push_str(chunk); | ||
| 106 | if buf.contains('\n') { | ||
| 107 | return Ok(n); | ||
| 108 | } | ||
| 109 | } | ||
| 110 | } | 117 | } |
| 111 | 118 | ||
| 112 | pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> { | 119 | pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> { |
| 113 | let mut stream = self.stream.lock().unwrap(); | 120 | self.inner.write_line(msg) |
| 114 | stream.write_all(msg.as_bytes())?; | ||
| 115 | stream.write_all(b"\n")?; | ||
| 116 | stream.flush()?; | ||
| 117 | Ok(()) | ||
| 118 | } | 121 | } |
| 119 | 122 | ||
| 120 | pub fn try_clone(&self) -> std::io::Result<TlsStream> { | 123 | pub fn try_clone(&self) -> std::io::Result<TlsStream> { |
| 121 | Ok(TlsStream { | 124 | Ok(TlsStream { |
| 122 | stream: Arc::clone(&self.stream), | 125 | inner: self.inner.try_clone()?, |
| 123 | }) | 126 | }) |
| 124 | } | 127 | } |
| 125 | } | 128 | } |
| 126 | 129 | ||
| 127 | impl Read for TlsStream { | 130 | impl Read for TlsStream { |
| 128 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { | 131 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { |
| 129 | let mut stream = self.stream.lock().unwrap(); | 132 | self.inner.read(buf) |
| 130 | stream.read(buf) | ||
| 131 | } | 133 | } |
| 132 | } | 134 | } |
| 133 | 135 | ||
| 134 | impl Write for TlsStream { | 136 | impl Write for TlsStream { |
| 135 | fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { | 137 | fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { |
| 136 | let mut stream = self.stream.lock().unwrap(); | 138 | self.inner.write(buf) |
| 137 | stream.write(buf) | ||
| 138 | } | 139 | } |
| 139 | 140 | ||
| 140 | fn flush(&mut self) -> std::io::Result<()> { | 141 | fn flush(&mut self) -> std::io::Result<()> { |
| 141 | let mut stream = self.stream.lock().unwrap(); | 142 | self.inner.flush() |
| 142 | stream.flush() | ||
| 143 | } | 143 | } |
| 144 | } | 144 | } |
