1use std::fmt;
13use std::fs;
14use std::path::{Path, PathBuf};
15use std::sync::{Arc, Mutex};
16use std::thread;
17
18use anyhow::{Context, Result, anyhow, bail};
19use rustls::client::WebPkiServerVerifier;
20use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
21use rustls::crypto::CryptoProvider;
22use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
23use rustls::{ClientConfig, DigitallySignedStruct, RootCertStore, SignatureScheme};
24use tokio::io::{AsyncReadExt, AsyncWriteExt};
25use tokio::net::TcpStream;
26use tokio::sync::mpsc;
27use tokio_rustls::TlsConnector;
28
29use crate::transport::{CONNECT_TIMEOUT, Cmd, Emitter, NetEvent, Writer, WriterKind};
30
31const CA_VERIFIED: &str = "ca";
33
34pub(crate) fn spawn_tls<F>(host: String, port: u16, pins: PathBuf, sink: F)
35where
36 F: Fn(NetEvent) + Send + 'static,
37{
38 thread::spawn(move || {
39 let emitter = Emitter::new(sink);
40 let rt = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
41 Ok(rt) => rt,
42 Err(e) => return emitter.emit(NetEvent::Closed(Some(format!("starting TLS runtime: {e}")))),
43 };
44 let result = rt.block_on(run_tls(&host, port, pins, emitter.clone()));
45 emitter.emit(NetEvent::Closed(result.err().map(|e| format!("{e:#}"))));
46 });
47}
48
49async fn run_tls(host: &str, port: u16, pins: PathBuf, emitter: Emitter) -> Result<()> {
50 let tcp = match tokio::time::timeout(CONNECT_TIMEOUT, TcpStream::connect((host, port))).await {
51 Err(_) => bail!("connecting to {host}:{port}: timed out"),
52 Ok(r) => r.with_context(|| format!("connecting to {host}:{port}"))?,
53 };
54 let _ = tcp.set_nodelay(true);
55
56 let verifier = Arc::new(Verifier::new(host, port, pins, emitter.clone())?);
57 let provider = Arc::new(rustls::crypto::ring::default_provider());
58 let config = ClientConfig::builder_with_provider(provider)
59 .with_safe_default_protocol_versions()?
60 .dangerous()
61 .with_custom_certificate_verifier(verifier.clone())
62 .with_no_client_auth();
63 let name = ServerName::try_from(host.to_string()).map_err(|_| anyhow!("{host} is not a valid host name"))?;
64 let handshake = TlsConnector::from(Arc::new(config)).connect(name, tcp);
65 let tls = match tokio::time::timeout(CONNECT_TIMEOUT, handshake).await {
66 Err(_) => bail!("TLS handshake with {host}:{port}: timed out"),
67 Ok(Err(e)) => {
68 if let Some(msg) = verifier.problem.lock().unwrap_or_else(|e| e.into_inner()).take() {
69 bail!(msg);
70 }
71 return Err(e).with_context(|| format!("TLS handshake with {host}:{port}"));
72 }
73 Ok(Ok(s)) => s,
74 };
75
76 let (tx, mut rx) = mpsc::unbounded_channel();
77 emitter.emit(NetEvent::Connected(Writer(WriterKind::Channel(tx))));
78 let (mut read, mut write) = tokio::io::split(tls);
79 let mut buf = vec![0u8; 16 * 1024];
80 loop {
81 tokio::select! {
82 n = read.read(&mut buf) => match n {
83 Ok(0) => break,
84 Ok(n) => emitter.emit(NetEvent::Data(buf[..n].to_vec())),
85 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
87 Err(e) => return Err(e).context("reading"),
88 },
89 cmd = rx.recv() => match cmd {
90 Some(Cmd::Data(d)) => {
91 write.write_all(&d).await.context("sending data")?;
92 write.flush().await.context("sending data")?;
93 }
94 Some(Cmd::Close) | None => {
95 let _ = write.shutdown().await;
96 break;
97 }
98 },
99 }
100 }
101 Ok(())
102}
103
104struct Verifier {
106 webpki: Arc<WebPkiServerVerifier>,
107 host: String,
108 port: u16,
109 pins: PathBuf,
110 emitter: Emitter,
111 problem: Mutex<Option<String>>,
113}
114
115impl fmt::Debug for Verifier {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 f.debug_struct("Verifier").field("host", &self.host).field("port", &self.port).finish()
118 }
119}
120
121impl Verifier {
122 fn new(host: &str, port: u16, pins: PathBuf, emitter: Emitter) -> Result<Verifier> {
123 let roots = RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
124 let provider: Arc<CryptoProvider> = Arc::new(rustls::crypto::ring::default_provider());
125 let webpki = WebPkiServerVerifier::builder_with_provider(Arc::new(roots), provider).build()?;
126 Ok(Verifier { webpki, host: host.to_string(), port, pins, emitter, problem: Mutex::new(None) })
127 }
128
129 fn refuse(&self, msg: String) -> rustls::Error {
130 *self.problem.lock().unwrap_or_else(|e| e.into_inner()) = Some(msg.clone());
131 rustls::Error::General(msg)
132 }
133}
134
135impl ServerCertVerifier for Verifier {
136 fn verify_server_cert(
137 &self,
138 end_entity: &CertificateDer<'_>,
139 intermediates: &[CertificateDer<'_>],
140 server_name: &ServerName<'_>,
141 ocsp_response: &[u8],
142 now: UnixTime,
143 ) -> Result<ServerCertVerified, rustls::Error> {
144 let ca = self.webpki.verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now);
145 let fingerprint = fingerprint(end_entity);
146 match check_pin(&self.pins, &self.host, self.port, &fingerprint, ca.is_ok()) {
147 Ok(notice) => {
148 if let Some(n) = notice {
149 self.emitter.emit(NetEvent::Notice(n));
150 }
151 Ok(ServerCertVerified::assertion())
152 }
153 Err(msg) => Err(self.refuse(msg)),
154 }
155 }
156
157 fn verify_tls12_signature(
158 &self,
159 message: &[u8],
160 cert: &CertificateDer<'_>,
161 dss: &DigitallySignedStruct,
162 ) -> Result<HandshakeSignatureValid, rustls::Error> {
163 self.webpki.verify_tls12_signature(message, cert, dss)
164 }
165
166 fn verify_tls13_signature(
167 &self,
168 message: &[u8],
169 cert: &CertificateDer<'_>,
170 dss: &DigitallySignedStruct,
171 ) -> Result<HandshakeSignatureValid, rustls::Error> {
172 self.webpki.verify_tls13_signature(message, cert, dss)
173 }
174
175 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
176 self.webpki.supported_verify_schemes()
177 }
178}
179
180fn fingerprint(cert: &CertificateDer<'_>) -> String {
182 let digest = ring::digest::digest(&ring::digest::SHA256, cert.as_ref());
183 let hex: String = digest.as_ref().iter().map(|b| format!("{b:02x}")).collect();
184 format!("sha256:{hex}")
185}
186
187pub fn check_pin(pins: &Path, host: &str, port: u16, fingerprint: &str, ca_ok: bool) -> Result<Option<String>, String> {
192 let key = pin_key(host, port);
193 let text = match fs::read_to_string(pins) {
194 Ok(t) => t,
195 Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
196 Err(e) => return Err(format!("reading {}: {e}", pins.display())),
197 };
198 let pinned = text.lines().find_map(|l| l.split_once(' ').filter(|(k, _)| *k == key).map(|(_, v)| v.trim()));
199 let save = |value: &str| -> Result<(), String> {
200 let mut lines: Vec<String> =
201 text.lines().filter(|l| l.split_once(' ').is_none_or(|(k, _)| k != key)).map(String::from).collect();
202 lines.push(format!("{key} {value}"));
203 if let Some(dir) = pins.parent() {
204 let _ = fs::create_dir_all(dir);
205 }
206 fs::write(pins, lines.join("\n") + "\n").map_err(|e| format!("saving {}: {e}", pins.display()))
207 };
208 match (pinned, ca_ok) {
209 (Some(CA_VERIFIED), true) => Ok(None),
210 (_, true) => save(CA_VERIFIED).map(|()| None),
212 (Some(CA_VERIFIED), false) => Err(format!(
213 "The TLS certificate of {host} is no longer signed by a certificate authority, as it was before. \
214 Someone may be intercepting the connection. If the BBS really changed it, remove the line for \
215 {key} from {}.",
216 pins.display()
217 )),
218 (Some(p), false) if p == fingerprint => Ok(None),
219 (Some(_), false) => Err(format!(
220 "The TLS certificate of {host} has CHANGED (now {fingerprint}). Someone may be intercepting the \
221 connection. If the BBS really changed it, remove the line for {key} from {}.",
222 pins.display()
223 )),
224 (None, false) => save(fingerprint).map(|()| {
225 Some(format!("Trusted the self-signed TLS certificate of {host} (first connection): {fingerprint}"))
226 }),
227 }
228}
229
230fn pin_key(host: &str, port: u16) -> String {
231 let host = host.to_ascii_lowercase();
232 if host.contains(':') { format!("[{host}]:{port}") } else { format!("{host}:{port}") }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn pins() {
241 let dir = std::env::temp_dir().join(format!("badterm-tls-pins-{}", std::process::id()));
242 let pins = dir.join("tls_known_hosts");
243 let _ = fs::remove_dir_all(&dir);
244
245 assert!(check_pin(&pins, "bbs.example", 992, "sha256:aa", false).unwrap().is_some());
247 assert_eq!(check_pin(&pins, "BBS.example", 992, "sha256:aa", false), Ok(None));
248 assert!(check_pin(&pins, "bbs.example", 992, "sha256:bb", false).unwrap_err().contains("CHANGED"));
249 assert!(check_pin(&pins, "bbs.example", 993, "sha256:bb", false).unwrap().is_some());
251
252 assert_eq!(check_pin(&pins, "bbs.example", 992, "sha256:cc", true), Ok(None));
254 assert!(check_pin(&pins, "bbs.example", 992, "sha256:aa", false).unwrap_err().contains("no longer"));
255 assert_eq!(check_pin(&pins, "other.example", 992, "sha256:dd", true), Ok(None));
256
257 let text = fs::read_to_string(&pins).unwrap();
258 assert_eq!(text, "bbs.example:993 sha256:bb\nbbs.example:992 ca\nother.example:992 ca\n");
259 assert_eq!(pin_key("::1", 992), "[::1]:992");
260 fs::remove_dir_all(&dir).unwrap();
261 }
262}