summaryrefslogtreecommitdiffstats
path: root/src/bin/client/tls.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/tls.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 '')
-rw-r--r--src/bin/client/tls.rs240
1 files changed, 153 insertions, 87 deletions
diff --git a/src/bin/client/tls.rs b/src/bin/client/tls.rs
index 8ab5c7c..ca99838 100644
--- a/src/bin/client/tls.rs
+++ b/src/bin/client/tls.rs
@@ -1,83 +1,128 @@
1//! rustls-based client TLS with Trust-On-First-Use (TOFU) host verification.
2//!
3//! Model (SSH known_hosts style):
4//! - First contact: accept the certificate and record the SHA-256 of its
5//! SubjectPublicKeyInfo (SPKI). Hashing the SPKI rather than the whole
6//! certificate means routine renewal with the same key does not break
7//! clients; key rotation does — by design.
8//! - Later contacts: the presented key must hash to the stored fingerprint,
9//! otherwise the handshake fails with a loud error.
10//!
11//! Handshake signatures are accepted without webpki validation; identity is
12//! established purely by the pinned fingerprint. An impostor cannot present a
13//! different certificate without triggering the mismatch rejection.
14
1use std::io::{Read, Write}; 15use std::io::{Read, Write};
2use std::net::TcpStream; 16use std::net::TcpStream;
3use std::sync::mpsc; 17use std::sync::Arc;
4use std::thread; 18
5use std::time::{Duration, Instant}; 19use parking_lot::Mutex;
20use rustls::client::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
21use rustls::DigitallySignedStruct;
22use zagan_bet::tofu::{decide, spki_fingerprint, TrustDecision};
6 23
7pub struct TlsConnector { 24pub struct TlsConnector {
8 connector: rustls::ClientConnection, 25 config: Arc<rustls::ClientConfig>,
9 server_name: String, 26 server_name: rustls::ServerName,
27 observed: Arc<Mutex<Option<String>>>,
10} 28}
11 29
12impl TlsConnector { 30impl TlsConnector {
13 pub fn new(server_name: &str) -> Result<Self, Box<dyn std::error::Error>> { 31 /// `stored` is the previously trusted fingerprint for this host, if any.
32 /// With `bypass_tofu` (unsafe mode) any certificate is accepted.
33 pub fn new(
34 server_name: &str,
35 stored: Option<String>,
36 bypass_tofu: bool,
37 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
38 let observed = Arc::new(Mutex::new(None));
39 let verifier = Arc::new(TofuVerifier {
40 stored,
41 bypass_tofu,
42 observed: Arc::clone(&observed),
43 });
14 let config = rustls::ClientConfig::builder() 44 let config = rustls::ClientConfig::builder()
15 .dangerous() 45 .with_safe_defaults()
16 .with_custom_certificate_verifier(std::sync::Arc::new(InsecureVerifier)) 46 .with_custom_certificate_verifier(verifier)
17 .with_no_client_auth(); 47 .with_no_client_auth();
18
19 let connector =
20 rustls::ClientConnection::new(std::sync::Arc::new(config), server_name.try_into()?);
21
22 Ok(Self { 48 Ok(Self {
23 connector, 49 config: Arc::new(config),
24 server_name: server_name.to_string(), 50 server_name: server_name.try_into()?,
51 observed,
25 }) 52 })
26 } 53 }
27 54
28 pub fn connect( 55 pub fn connect(&self, stream: TcpStream) -> Result<TlsStream, Box<dyn std::error::Error + Send + Sync>> {
29 &self, 56 let mut conn =
30 stream: TcpStream, 57 rustls::ClientConnection::new(Arc::clone(&self.config), self.server_name.clone())?;
31 ) -> Result<TlsClientStream, Box<dyn std::error::Error>> { 58 let mut sock = stream;
32 let mut conn = rustls::ClientConnection::new( 59 conn.complete_io(&mut sock)?;
33 std::sync::Arc::new( 60
34 rustls::ClientConfig::builder() 61 Ok(TlsStream {
35 .dangerous() 62 session: Arc::new(Mutex::new(Session { sock, tls: conn })),
36 .with_custom_verifier(rustls::client::danger::ServerCertVerifier::DEFAULT)
37 .with_no_client_auth(),
38 ),
39 self.server_name.clone().try_into()?,
40 )?;
41
42 conn.complete_io(&mut std::io::BufReader::new(&stream))?;
43
44 Ok(TlsClientStream {
45 stream,
46 conn: std::sync::Arc::new(conn),
47 }) 63 })
48 } 64 }
65
66 /// Fingerprint of the certificate seen during the last `connect`.
67 pub fn observed_fingerprint(&self) -> Option<String> {
68 self.observed.lock().clone()
69 }
49} 70}
50 71
51struct InsecureVerifier; 72struct TofuVerifier {
73 stored: Option<String>,
74 bypass_tofu: bool,
75 observed: Arc<Mutex<Option<String>>>,
76}
52 77
53impl rustls::client::danger::ServerCertVerifier for InsecureVerifier { 78impl ServerCertVerifier for TofuVerifier {
54 fn verify_server_cert( 79 fn verify_server_cert(
55 &self, 80 &self,
56 _end_entity: &rustls::pki_types::CertificateDer<'_>, 81 end_entity: &rustls::Certificate,
57 _intermediates: &[rustls::pki_types::CertificateDer<'_>], 82 _intermediates: &[rustls::Certificate],
58 _server_name: &rustls::pki_types::ServerName<'_>, 83 _server_name: &rustls::ServerName,
84 _scts: &mut dyn Iterator<Item = &[u8]>,
59 _ocsp_response: &[u8], 85 _ocsp_response: &[u8],
60 _now: std::time::SystemTime, 86 _now: std::time::SystemTime,
61 ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> { 87 ) -> Result<ServerCertVerified, rustls::Error> {
62 Ok(rustls::client::danger::ServerCertVerified::assertion()) 88 let fp = spki_fingerprint(&end_entity.0)
89 .map_err(|e| rustls::Error::General(format!("certificate parse failed: {e}")))?;
90 *self.observed.lock() = Some(fp.clone());
91
92 if self.bypass_tofu {
93 return Ok(ServerCertVerified::assertion());
94 }
95
96 match decide(self.stored.as_deref(), &fp) {
97 TrustDecision::TrustNew | TrustDecision::Trusted => Ok(ServerCertVerified::assertion()),
98 TrustDecision::Mismatch { expected, actual } => Err(rustls::Error::General(format!(
99 "HOST KEY CHANGED for this server!\n\
100 expected fingerprint: {expected}\n\
101 observed fingerprint: {actual}\n\
102 If this is intentional (server reinstall), use the client's\n\
103 forget-host command to reset trust for this address."
104 ))),
105 }
63 } 106 }
64 107
108 // Identity is pinned via fingerprint comparison above; handshake
109 // signatures are not additionally verified against a trust store.
65 fn verify_tls12_signature( 110 fn verify_tls12_signature(
66 &self, 111 &self,
67 _message: &[u8], 112 _message: &[u8],
68 _cert: &rustls::pki_types::CertificateDer<'_>, 113 _cert: &rustls::Certificate,
69 _dss: &rustls::DigitallySignedStruct, 114 _dss: &DigitallySignedStruct,
70 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> { 115 ) -> Result<HandshakeSignatureValid, rustls::Error> {
71 Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) 116 Ok(HandshakeSignatureValid::assertion())
72 } 117 }
73 118
74 fn verify_tls13_signature( 119 fn verify_tls13_signature(
75 &self, 120 &self,
76 _message: &[u8], 121 _message: &[u8],
77 _cert: &rustls::pki_types::CertificateDer<'_>, 122 _cert: &rustls::Certificate,
78 _dss: &rustls::DigitallySignedStruct, 123 _dss: &DigitallySignedStruct,
79 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> { 124 ) -> Result<HandshakeSignatureValid, rustls::Error> {
80 Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) 125 Ok(HandshakeSignatureValid::assertion())
81 } 126 }
82 127
83 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> { 128 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
@@ -91,60 +136,81 @@ impl rustls::client::danger::ServerCertVerifier for InsecureVerifier {
91 rustls::SignatureScheme::ECDSA_NISTP256_SHA256, 136 rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
92 rustls::SignatureScheme::ECDSA_NISTP384_SHA384, 137 rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
93 rustls::SignatureScheme::ECDSA_NISTP521_SHA512, 138 rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
94 rustls::SignatureScheme::RSA_PSS_SHA256_LEGACY,
95 rustls::SignatureScheme::RSA_PSS_SHA384_LEGACY,
96 rustls::SignatureScheme::RSA_PSS_SHA512_LEGACY,
97 ] 139 ]
98 } 140 }
99} 141}
100 142
101pub struct TlsClientStream { 143/// TCP socket + TLS session behind one mutex so clones share the stream.
102 stream: TcpStream, 144struct Session {
103 conn: std::sync::Arc<rustls::ClientConnection>, 145 sock: TcpStream,
146 tls: rustls::ClientConnection,
147}
148
149impl Session {
150 fn drive(&mut self) -> std::io::Result<()> {
151 Ok(self.tls.complete_io(&mut self.sock).map(|_| ())?)
152 }
104} 153}
105 154
106impl TlsClientStream { 155#[derive(Clone)]
156pub struct TlsStream {
157 session: Arc<Mutex<Session>>,
158}
159
160impl TlsStream {
107 pub fn read_line(&mut self, buf: &mut String) -> std::io::Result<usize> { 161 pub fn read_line(&mut self, buf: &mut String) -> std::io::Result<usize> {
108 let mut byte_buf = [0u8; 1024]; 162 let mut byte_buf = [0u8; 1024];
163 let mut s = self.session.lock();
109 loop { 164 loop {
110 let mut tls_reader = std::io::BufReader::new(self.conn.clone()); 165 match s.tls.reader().read(&mut byte_buf) {
111 let n = tls_reader.read(&mut byte_buf)?; 166 Ok(0) => return Ok(0),
112 if n == 0 { 167 Ok(n) => {
113 return Ok(0); 168 buf.push_str(std::str::from_utf8(&byte_buf[..n]).unwrap_or(""));
114 } 169 if buf.contains('\n') {
115 let chunk = std::str::from_utf8(&byte_buf[..n]).unwrap_or(""); 170 return Ok(n);
116 buf.push_str(chunk); 171 }
117 if buf.contains('\n') { 172 }
118 return Ok(n); 173 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => s.drive()?,
174 Err(e) => return Err(e),
119 } 175 }
120 } 176 }
121 } 177 }
122 178
123 pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> { 179 pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> {
124 use std::io::Write; 180 let mut s = self.session.lock();
125 self.conn.writer().write_all(msg.as_bytes())?; 181 s.tls.writer().write_all(msg.as_bytes())?;
126 self.conn.writer().write_all(b"\n")?; 182 s.tls.writer().write_all(b"\n")?;
127 self.conn.writer().flush()?; 183 s.drive()
128 Ok(())
129 } 184 }
130 185
131 pub fn try_clone(&self) -> std::io::Result<TlsClientStream> { 186 /// Clones share the same underlying TLS session.
132 let new_stream = self.stream.try_clone()?; 187 pub fn try_clone(&self) -> std::io::Result<TlsStream> {
133 let mut conn = rustls::ClientConnection::new( 188 Ok(self.clone())
134 std::sync::Arc::new( 189 }
135 rustls::ClientConfig::builder() 190}
136 .dangerous() 191
137 .with_custom_verifier(rustls::client::danger::ServerCertVerifier::DEFAULT) 192impl Read for TlsStream {
138 .with_no_client_auth(), 193 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
139 ), 194 let mut s = self.session.lock();
140 self.server_name.clone().try_into().unwrap(), 195 loop {
141 )?; 196 match s.tls.reader().read(buf) {
142 197 Ok(n) => return Ok(n),
143 conn.complete_io(&mut std::io::BufReader::new(&new_stream))?; 198 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => s.drive()?,
144 199 Err(e) => return Err(e),
145 Ok(TlsClientStream { 200 }
146 stream: new_stream, 201 }
147 conn: std::sync::Arc::new(conn), 202 }
148 }) 203}
204
205impl Write for TlsStream {
206 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
207 let mut s = self.session.lock();
208 s.tls.writer().write(buf)
209 }
210
211 fn flush(&mut self) -> std::io::Result<()> {
212 let mut s = self.session.lock();
213 s.tls.writer().flush()?;
214 s.drive()
149 } 215 }
150} 216}