summaryrefslogtreecommitdiffstats
path: root/src/bin/client/connection.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-24 15:27:06 +0200
committersalmonik <salmonik@tuta.io>2026-08-24 15:27:06 +0200
commit8218fd584e4053848738a310258893e14c4ee10e (patch)
tree2c92997ba0b6aa260fe72c88190f5f8b590457cb /src/bin/client/connection.rs
parentTLS transport and games framework on client and server (diff)
downloadzagan-bet-8218fd584e4053848738a310258893e14c4ee10e.tar.gz
zagan-bet-8218fd584e4053848738a310258893e14c4ee10e.zip
TOFU host trust: SPKI fingerprint pinning, tests, forget-host
Diffstat (limited to 'src/bin/client/connection.rs')
-rw-r--r--src/bin/client/connection.rs174
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 @@
1use crate::database; 1use crate::database;
2use native_tls::TlsConnector; 2use crate::tls::TlsConnector;
3use rusqlite::Connection; 3use rusqlite::Connection;
4use std::io::{Read, Write}; 4use std::io::{Read, Write};
5use std::net::TcpStream; 5use std::net::TcpStream;
6use std::sync::mpsc; 6use std::sync::mpsc;
7use std::sync::Arc; 7use parking_lot::Mutex;
8use std::sync::Mutex;
9use std::thread; 8use std::thread;
10use std::time::{Duration, Instant}; 9use std::time::{Duration, Instant};
11 10
12static UNSAFE_MODE: Mutex<bool> = Mutex::new(false); 11static UNSAFE_MODE: Mutex<bool> = Mutex::new(false);
13 12
14pub fn set_unsafe_mode(unsafe_mode: bool) { 13pub 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
17type ConnectResult = Result<(TlsStream, Option<String>), Box<dyn std::error::Error + Send + Sync>>;
18
19pub fn connect( 19pub 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
82pub 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
86unsafe impl Send for TlsStream {} 94/// Wrapper so TLS handshake failures surface as errors with their message
87unsafe impl Sync for TlsStream {} 95/// intact (rustls errors already carry the mismatch details).
96struct TrustError(String);
88 97
89impl TlsStream { 98impl 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}
103impl std::fmt::Debug for TrustError {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 write!(f, "{self}")
106 }
107}
108impl std::error::Error for TrustError {}
109
110pub struct TlsStream {
111 inner: crate::tls::TlsStream,
112}
95 113
114impl 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
127impl Read for TlsStream { 130impl 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
134impl Write for TlsStream { 136impl 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}