summaryrefslogtreecommitdiffstats
path: root/src/client/transport.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/transport.rs')
-rw-r--r--src/client/transport.rs447
1 files changed, 447 insertions, 0 deletions
diff --git a/src/client/transport.rs b/src/client/transport.rs
new file mode 100644
index 0000000..97c9227
--- /dev/null
+++ b/src/client/transport.rs
@@ -0,0 +1,447 @@
1//! Client transport: TCP, rustls, TOFU and the HELLO handshake.
2//!
3//! The handshake records the server's SPKI fingerprint but does not act on it;
4//! the trust decision happens after the handshake and before any credentials
5//! are sent, so a frontend can ask the player. A changed key is a hard error.
6
7use std::fmt;
8use std::io::{Read, Write};
9use std::net::TcpStream;
10use std::sync::mpsc;
11use std::sync::Arc;
12use std::thread;
13use std::time::{Duration, Instant};
14
15use parking_lot::Mutex;
16use rustls::client::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
17use rustls::DigitallySignedStruct;
18use rusqlite::Connection;
19
20use crate::client::database;
21use crate::protocol::LineBuffer;
22use crate::tofu::{decide, spki_fingerprint, TrustDecision};
23use crate::version::{parse_hello, HelloReply, VERSION};
24
25/// How long to wait for TCP + TLS before giving up.
26const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
27/// Read timeout once the stream is usable.
28const READ_TIMEOUT: Duration = Duration::from_secs(10);
29
30/// What a frontend shows when a server key is unknown.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct TrustPrompt {
33 pub addr: String,
34 pub fingerprint: String,
35}
36
37/// A usable connection after trust, version and optional auto-login.
38pub struct Connected {
39 pub stream: TlsStream,
40 pub user: Option<String>,
41 /// Set when stored credentials were rejected; the session is still usable.
42 pub auth_error: Option<String>,
43 pub server_version: String,
44 pub latest: String,
45 pub url: String,
46 /// Round trip of the HELLO exchange.
47 pub elapsed: Duration,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum TransportError {
52 Timeout,
53 TrustDeclined,
54 KeyChanged {
55 expected: String,
56 observed: String,
57 },
58 VersionMismatch(String),
59 Closed,
60 Unexpected(String),
61 Io(String),
62}
63
64impl fmt::Display for TransportError {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 match self {
67 TransportError::Timeout => write!(f, "Connection timed out"),
68 TransportError::TrustDeclined => write!(f, "Certificate was not trusted"),
69 TransportError::KeyChanged { expected, observed } => write!(
70 f,
71 "HOST KEY CHANGED for this server!\n\
72 expected fingerprint: {expected}\n\
73 observed fingerprint: {observed}\n\
74 If this is intentional (server reinstall), forget this host to reset trust."
75 ),
76 TransportError::VersionMismatch(message) => write!(f, "{message}"),
77 TransportError::Closed => write!(f, "Server closed the connection during HELLO"),
78 TransportError::Unexpected(message) => write!(f, "{message}"),
79 TransportError::Io(message) => write!(f, "Connection error: {message}"),
80 }
81 }
82}
83
84impl std::error::Error for TransportError {}
85
86fn io_failure(error: std::io::Error) -> TransportError {
87 TransportError::Io(error.to_string())
88}
89
90/// Connect, verify the key, check versions and try the stored login.
91///
92/// `trust` is called only for a server we have never seen; returning `false`
93/// aborts before any application data is sent.
94pub fn connect(
95 addr: &str,
96 conn: &Connection,
97 unsafe_mode: bool,
98 trust: &mut dyn FnMut(&TrustPrompt) -> bool,
99) -> Result<Connected, TransportError> {
100 let stored = database::get_fingerprint(conn, addr);
101 let (tls, observed) = tls_connect(addr)?;
102 let observed = observed.ok_or_else(|| {
103 TransportError::Unexpected("the server presented no certificate fingerprint".to_string())
104 })?;
105
106 if !unsafe_mode {
107 match decide(stored.as_deref(), &observed) {
108 TrustDecision::Trusted => {}
109 TrustDecision::TrustNew => {
110 let prompt = TrustPrompt {
111 addr: addr.to_string(),
112 fingerprint: observed.clone(),
113 };
114 if !trust(&prompt) {
115 return Err(TransportError::TrustDeclined);
116 }
117 database::save_fingerprint(conn, addr, &observed)
118 .map_err(|error| TransportError::Io(error.to_string()))?;
119 }
120 TrustDecision::Mismatch { expected, actual } => {
121 return Err(TransportError::KeyChanged {
122 expected,
123 observed: actual,
124 })
125 }
126 }
127 }
128
129 let mut tls = tls;
130 tls.set_read_timeout(Some(READ_TIMEOUT))
131 .map_err(io_failure)?;
132
133 let start = Instant::now();
134 tls.write_line(&format!("HELLO {VERSION}"))
135 .map_err(io_failure)?;
136 let mut response = String::new();
137 if tls.read_line(&mut response).map_err(io_failure)? == 0 {
138 return Err(TransportError::Closed);
139 }
140 let elapsed = start.elapsed();
141
142 let response = response.trim();
143 if let Some(message) = response.strip_prefix("ERROR") {
144 return Err(TransportError::VersionMismatch(message.trim().to_string()));
145 }
146 let hello: HelloReply =
147 parse_hello(response).ok_or_else(|| TransportError::Unexpected(response.to_string()))?;
148
149 // Credentials are keyed by the pinned fingerprint, so any address that
150 // presents this key can log in with the same stored account.
151 let mut user = None;
152 let mut auth_error = None;
153 if let Some((username, password)) = database::get_credentials(conn, &observed) {
154 tls.write_line(&format!("AUTH {username} {password}"))
155 .map_err(io_failure)?;
156 let mut auth = String::new();
157 tls.read_line(&mut auth).map_err(io_failure)?;
158 if auth.trim().starts_with("OK") {
159 user = Some(username);
160 } else {
161 auth_error = Some(auth.trim().to_string());
162 }
163 }
164
165 Ok(Connected {
166 stream: tls,
167 user,
168 auth_error,
169 server_version: hello.version,
170 latest: hello.latest,
171 url: hello.url,
172 elapsed,
173 })
174}
175
176/// TCP + TLS on a thread so a blocked handshake can time out.
177fn tls_connect(addr: &str) -> Result<(TlsStream, Option<String>), TransportError> {
178 let (sender, receiver) = mpsc::channel();
179 let addr = addr.to_string();
180 let server_name = addr
181 .split(':')
182 .next()
183 .unwrap_or("127.0.0.1")
184 .to_string();
185
186 thread::spawn(move || {
187 let outcome = (|| -> Result<(TlsStream, Option<String>), TransportError> {
188 let tcp = TcpStream::connect(&addr).map_err(io_failure)?;
189 let connector = TlsConnector::new(&server_name)
190 .map_err(|error| TransportError::Io(error.to_string()))?;
191 let stream = connector
192 .connect(tcp)
193 .map_err(|error| TransportError::Io(error.to_string()))?;
194 Ok((stream, connector.observed_fingerprint()))
195 })();
196 let _ = sender.send(outcome);
197 });
198
199 match receiver.recv_timeout(CONNECT_TIMEOUT) {
200 Ok(outcome) => outcome,
201 Err(mpsc::RecvTimeoutError::Timeout) => Err(TransportError::Timeout),
202 Err(mpsc::RecvTimeoutError::Disconnected) => Err(TransportError::Unexpected(
203 "connection attempt failed".to_string(),
204 )),
205 }
206}
207
208struct TlsConnector {
209 config: Arc<rustls::ClientConfig>,
210 server_name: rustls::ServerName,
211 observed: Arc<Mutex<Option<String>>>,
212}
213
214impl TlsConnector {
215 fn new(server_name: &str) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
216 let observed = Arc::new(Mutex::new(None));
217 let verifier = Arc::new(RecordingVerifier {
218 observed: Arc::clone(&observed),
219 });
220 let config = rustls::ClientConfig::builder()
221 .with_safe_defaults()
222 .with_custom_certificate_verifier(verifier)
223 .with_no_client_auth();
224 Ok(Self {
225 config: Arc::new(config),
226 server_name: server_name.try_into()?,
227 observed,
228 })
229 }
230
231 fn connect(
232 &self,
233 stream: TcpStream,
234 ) -> Result<TlsStream, Box<dyn std::error::Error + Send + Sync>> {
235 let mut connection =
236 rustls::ClientConnection::new(Arc::clone(&self.config), self.server_name.clone())?;
237 let mut sock = stream;
238 connection.complete_io(&mut sock)?;
239
240 Ok(TlsStream {
241 session: Arc::new(Mutex::new(Session {
242 sock,
243 tls: connection,
244 lines: LineBuffer::new(),
245 read_timeout: Some(READ_TIMEOUT),
246 })),
247 })
248 }
249
250 fn observed_fingerprint(&self) -> Option<String> {
251 self.observed.lock().clone()
252 }
253}
254
255/// Certificate verification is record-only: the trust decision is made after
256/// the handshake, before anything is sent, so a frontend can ask first.
257struct RecordingVerifier {
258 observed: Arc<Mutex<Option<String>>>,
259}
260
261impl ServerCertVerifier for RecordingVerifier {
262 fn verify_server_cert(
263 &self,
264 end_entity: &rustls::Certificate,
265 _intermediates: &[rustls::Certificate],
266 _server_name: &rustls::ServerName,
267 _scts: &mut dyn Iterator<Item = &[u8]>,
268 _ocsp_response: &[u8],
269 _now: std::time::SystemTime,
270 ) -> Result<ServerCertVerified, rustls::Error> {
271 let fingerprint = spki_fingerprint(&end_entity.0)
272 .map_err(|error| rustls::Error::General(format!("certificate parse failed: {error}")))?;
273 *self.observed.lock() = Some(fingerprint);
274 Ok(ServerCertVerified::assertion())
275 }
276
277 // Identity is pinned by the fingerprint check above; handshake signatures
278 // are not additionally validated against a trust store.
279 fn verify_tls12_signature(
280 &self,
281 _message: &[u8],
282 _cert: &rustls::Certificate,
283 _dss: &DigitallySignedStruct,
284 ) -> Result<HandshakeSignatureValid, rustls::Error> {
285 Ok(HandshakeSignatureValid::assertion())
286 }
287
288 fn verify_tls13_signature(
289 &self,
290 _message: &[u8],
291 _cert: &rustls::Certificate,
292 _dss: &DigitallySignedStruct,
293 ) -> Result<HandshakeSignatureValid, rustls::Error> {
294 Ok(HandshakeSignatureValid::assertion())
295 }
296
297 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
298 vec![
299 rustls::SignatureScheme::RSA_PKCS1_SHA256,
300 rustls::SignatureScheme::RSA_PKCS1_SHA384,
301 rustls::SignatureScheme::RSA_PKCS1_SHA512,
302 rustls::SignatureScheme::RSA_PSS_SHA256,
303 rustls::SignatureScheme::RSA_PSS_SHA384,
304 rustls::SignatureScheme::RSA_PSS_SHA512,
305 rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
306 rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
307 rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
308 ]
309 }
310}
311
312/// TCP socket + TLS session behind one mutex so clones share the stream.
313struct Session {
314 sock: TcpStream,
315 tls: rustls::ClientConnection,
316 lines: LineBuffer,
317 read_timeout: Option<Duration>,
318}
319
320impl Session {
321 fn drive(&mut self) -> std::io::Result<()> {
322 self.tls.complete_io(&mut self.sock).map(|_| ())
323 }
324}
325
326#[derive(Clone)]
327pub struct TlsStream {
328 session: Arc<Mutex<Session>>,
329}
330
331impl TlsStream {
332 pub fn read_line(&mut self, buf: &mut String) -> std::io::Result<usize> {
333 let mut s = self.session.lock();
334 loop {
335 if let Some(line) = s.lines.next_line() {
336 buf.push_str(&line);
337 buf.push('\n');
338 return Ok(line.len() + 1);
339 }
340
341 let mut chunk = [0u8; 4096];
342 match s.tls.reader().read(&mut chunk) {
343 Ok(0) => {
344 if let Some(rest) = s.lines.take_rest() {
345 let read = rest.len();
346 buf.push_str(&rest);
347 return Ok(read);
348 }
349 return Ok(0);
350 }
351 Ok(n) => s.lines.push(&chunk[..n]),
352 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => s.drive()?,
353 Err(error) => return Err(error),
354 }
355 }
356 }
357
358 pub fn write_line(&mut self, message: &str) -> std::io::Result<()> {
359 let mut s = self.session.lock();
360 s.tls.writer().write_all(message.as_bytes())?;
361 s.tls.writer().write_all(b"\n")?;
362 s.drive()
363 }
364
365 pub fn set_read_timeout(&self, duration: Option<Duration>) -> std::io::Result<()> {
366 let mut s = self.session.lock();
367 s.read_timeout = duration;
368 s.sock.set_read_timeout(duration)
369 }
370
371 /// Return one complete line if the socket already has one, without
372 /// blocking. Used to pick up server pushes between requests.
373 pub fn read_available_line(&mut self) -> std::io::Result<Option<String>> {
374 let mut s = self.session.lock();
375 if let Some(line) = s.lines.next_line() {
376 return Ok(Some(line));
377 }
378
379 // `set_read_timeout(Some(0))` is rejected by std, so poll with the
380 // shortest timeout the socket accepts instead of a nonblocking mode.
381 s.sock
382 .set_read_timeout(Some(std::time::Duration::from_millis(1)))?;
383 let drained = (|| -> std::io::Result<()> {
384 loop {
385 let mut buf = [0u8; 4096];
386 match s.tls.reader().read(&mut buf) {
387 Ok(0) => return Ok(()),
388 Ok(n) => s.lines.push(&buf[..n]),
389 // The reader only drains plaintext rustls has already
390 // decrypted; `drive` moves bytes off the socket.
391 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
392 match s.drive() {
393 Ok(()) => continue,
394 Err(error)
395 if matches!(
396 error.kind(),
397 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
398 ) =>
399 {
400 return Ok(());
401 }
402 Err(error) => return Err(error),
403 }
404 }
405 Err(error)
406 if matches!(
407 error.kind(),
408 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
409 ) =>
410 {
411 return Ok(());
412 }
413 Err(error) => return Err(error),
414 }
415 }
416 })();
417 s.sock.set_read_timeout(s.read_timeout)?;
418 drained?;
419 Ok(s.lines.next_line())
420 }
421}
422
423impl Read for TlsStream {
424 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
425 let mut s = self.session.lock();
426 loop {
427 match s.tls.reader().read(buf) {
428 Ok(n) => return Ok(n),
429 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => s.drive()?,
430 Err(error) => return Err(error),
431 }
432 }
433 }
434}
435
436impl Write for TlsStream {
437 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
438 let mut s = self.session.lock();
439 s.tls.writer().write(buf)
440 }
441
442 fn flush(&mut self) -> std::io::Result<()> {
443 let mut s = self.session.lock();
444 s.tls.writer().flush()?;
445 s.drive()
446 }
447}