summaryrefslogtreecommitdiffstats
path: root/src/bin/client/tls.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/bin/client/tls.rs288
1 files changed, 0 insertions, 288 deletions
diff --git a/src/bin/client/tls.rs b/src/bin/client/tls.rs
deleted file mode 100644
index e14ed98..0000000
--- a/src/bin/client/tls.rs
+++ /dev/null
@@ -1,288 +0,0 @@
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
15use std::io::{Read, Write};
16use std::net::TcpStream;
17use std::sync::Arc;
18
19use parking_lot::Mutex;
20use rustls::DigitallySignedStruct;
21use rustls::client::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
22use zagan_bet::protocol::LineBuffer;
23use zagan_bet::tofu::{TrustDecision, decide, spki_fingerprint};
24
25pub struct TlsConnector {
26 config: Arc<rustls::ClientConfig>,
27 server_name: rustls::ServerName,
28 observed: Arc<Mutex<Option<String>>>,
29}
30
31impl TlsConnector {
32 /// `stored` is the previously trusted fingerprint for this host, if any.
33 /// With `bypass_tofu` (unsafe mode) any certificate is accepted.
34 pub fn new(
35 server_name: &str,
36 stored: Option<String>,
37 bypass_tofu: bool,
38 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
39 let observed = Arc::new(Mutex::new(None));
40 let verifier = Arc::new(TofuVerifier {
41 stored,
42 bypass_tofu,
43 observed: Arc::clone(&observed),
44 });
45 let config = rustls::ClientConfig::builder()
46 .with_safe_defaults()
47 .with_custom_certificate_verifier(verifier)
48 .with_no_client_auth();
49 Ok(Self {
50 config: Arc::new(config),
51 server_name: server_name.try_into()?,
52 observed,
53 })
54 }
55
56 pub fn connect(
57 &self,
58 stream: TcpStream,
59 ) -> Result<TlsStream, Box<dyn std::error::Error + Send + Sync>> {
60 let mut conn =
61 rustls::ClientConnection::new(Arc::clone(&self.config), self.server_name.clone())?;
62 let mut sock = stream;
63 conn.complete_io(&mut sock)?;
64
65 Ok(TlsStream {
66 session: Arc::new(Mutex::new(Session {
67 sock,
68 tls: conn,
69 lines: LineBuffer::new(),
70 read_timeout: Some(std::time::Duration::from_secs(10)),
71 })),
72 })
73 }
74
75 /// Fingerprint of the certificate seen during the last `connect`.
76 pub fn observed_fingerprint(&self) -> Option<String> {
77 self.observed.lock().clone()
78 }
79}
80
81struct TofuVerifier {
82 stored: Option<String>,
83 bypass_tofu: bool,
84 observed: Arc<Mutex<Option<String>>>,
85}
86
87impl ServerCertVerifier for TofuVerifier {
88 fn verify_server_cert(
89 &self,
90 end_entity: &rustls::Certificate,
91 _intermediates: &[rustls::Certificate],
92 _server_name: &rustls::ServerName,
93 _scts: &mut dyn Iterator<Item = &[u8]>,
94 _ocsp_response: &[u8],
95 _now: std::time::SystemTime,
96 ) -> Result<ServerCertVerified, rustls::Error> {
97 let fp = spki_fingerprint(&end_entity.0)
98 .map_err(|e| rustls::Error::General(format!("certificate parse failed: {e}")))?;
99 *self.observed.lock() = Some(fp.clone());
100
101 if self.bypass_tofu {
102 return Ok(ServerCertVerified::assertion());
103 }
104
105 match decide(self.stored.as_deref(), &fp) {
106 TrustDecision::TrustNew | TrustDecision::Trusted => Ok(ServerCertVerified::assertion()),
107 TrustDecision::Mismatch { expected, actual } => Err(rustls::Error::General(format!(
108 "HOST KEY CHANGED for this server!\n\
109 expected fingerprint: {expected}\n\
110 observed fingerprint: {actual}\n\
111 If this is intentional (server reinstall), use the client's\n\
112 forget-host command to reset trust for this address."
113 ))),
114 }
115 }
116
117 // Identity is pinned via fingerprint comparison above; handshake
118 // signatures are not additionally verified against a trust store.
119 fn verify_tls12_signature(
120 &self,
121 _message: &[u8],
122 _cert: &rustls::Certificate,
123 _dss: &DigitallySignedStruct,
124 ) -> Result<HandshakeSignatureValid, rustls::Error> {
125 Ok(HandshakeSignatureValid::assertion())
126 }
127
128 fn verify_tls13_signature(
129 &self,
130 _message: &[u8],
131 _cert: &rustls::Certificate,
132 _dss: &DigitallySignedStruct,
133 ) -> Result<HandshakeSignatureValid, rustls::Error> {
134 Ok(HandshakeSignatureValid::assertion())
135 }
136
137 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
138 vec![
139 rustls::SignatureScheme::RSA_PKCS1_SHA256,
140 rustls::SignatureScheme::RSA_PKCS1_SHA384,
141 rustls::SignatureScheme::RSA_PKCS1_SHA512,
142 rustls::SignatureScheme::RSA_PSS_SHA256,
143 rustls::SignatureScheme::RSA_PSS_SHA384,
144 rustls::SignatureScheme::RSA_PSS_SHA512,
145 rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
146 rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
147 rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
148 ]
149 }
150}
151
152/// TCP socket + TLS session behind one mutex so clones share the stream.
153struct Session {
154 sock: TcpStream,
155 tls: rustls::ClientConnection,
156 lines: LineBuffer,
157 read_timeout: Option<std::time::Duration>,
158}
159
160impl Session {
161 fn drive(&mut self) -> std::io::Result<()> {
162 Ok(self.tls.complete_io(&mut self.sock).map(|_| ())?)
163 }
164}
165
166#[derive(Clone)]
167pub struct TlsStream {
168 session: Arc<Mutex<Session>>,
169}
170
171impl TlsStream {
172 pub fn read_line(&mut self, buf: &mut String) -> std::io::Result<usize> {
173 let mut s = self.session.lock();
174 loop {
175 if let Some(line) = s.lines.next_line() {
176 buf.push_str(&line);
177 buf.push('\n');
178 return Ok(line.len() + 1);
179 }
180
181 let mut byte_buf = [0u8; 4096];
182 match s.tls.reader().read(&mut byte_buf) {
183 Ok(0) => {
184 if let Some(rest) = s.lines.take_rest() {
185 let read = rest.len();
186 buf.push_str(&rest);
187 return Ok(read);
188 }
189 return Ok(0);
190 }
191 Ok(n) => s.lines.push(&byte_buf[..n]),
192 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => s.drive()?,
193 Err(e) => return Err(e),
194 }
195 }
196 }
197
198 pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> {
199 let mut s = self.session.lock();
200 s.tls.writer().write_all(msg.as_bytes())?;
201 s.tls.writer().write_all(b"\n")?;
202 s.drive()
203 }
204
205 /// Apply a read timeout to the underlying TCP socket.
206 pub fn set_read_timeout(&self, duration: Option<std::time::Duration>) -> std::io::Result<()> {
207 let mut s = self.session.lock();
208 s.read_timeout = duration;
209 s.sock.set_read_timeout(duration)
210 }
211
212 /// Return one complete line if the socket already has one, without
213 /// blocking. Used to pick up server pushes between requests.
214 pub fn read_available_line(&mut self) -> std::io::Result<Option<String>> {
215 let mut s = self.session.lock();
216 if let Some(line) = s.lines.next_line() {
217 return Ok(Some(line));
218 }
219
220 // `set_read_timeout(Some(0))` is rejected by std, so poll with the
221 // shortest timeout the socket accepts instead of a nonblocking mode.
222 s.sock
223 .set_read_timeout(Some(std::time::Duration::from_millis(1)))?;
224 let drained = (|| -> std::io::Result<()> {
225 loop {
226 let mut buf = [0u8; 4096];
227 match s.tls.reader().read(&mut buf) {
228 Ok(0) => return Ok(()),
229 Ok(n) => s.lines.push(&buf[..n]),
230 // The reader only drains plaintext rustls has already
231 // decrypted; `drive` moves bytes off the socket.
232 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
233 match s.drive() {
234 Ok(()) => continue,
235 Err(error)
236 if matches!(
237 error.kind(),
238 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
239 ) =>
240 {
241 return Ok(());
242 }
243 Err(error) => return Err(error),
244 }
245 }
246 Err(error)
247 if matches!(
248 error.kind(),
249 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
250 ) =>
251 {
252 return Ok(());
253 }
254 Err(error) => return Err(error),
255 }
256 }
257 })();
258 s.sock.set_read_timeout(s.read_timeout)?;
259 drained?;
260 Ok(s.lines.next_line())
261 }
262}
263
264impl Read for TlsStream {
265 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
266 let mut s = self.session.lock();
267 loop {
268 match s.tls.reader().read(buf) {
269 Ok(n) => return Ok(n),
270 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => s.drive()?,
271 Err(e) => return Err(e),
272 }
273 }
274 }
275}
276
277impl Write for TlsStream {
278 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
279 let mut s = self.session.lock();
280 s.tls.writer().write(buf)
281 }
282
283 fn flush(&mut self) -> std::io::Result<()> {
284 let mut s = self.session.lock();
285 s.tls.writer().flush()?;
286 s.drive()
287 }
288}