Skip to main content

badterm_core/
tls.rs

1//! Telnet over TLS ("TelnetS", usually port 992), on rustls with the same
2//! ring crypto as SSH.
3//!
4//! A certificate is first checked against the web's certificate authorities
5//! (the Mozilla roots in `webpki-roots`). Many BBSes use self-signed
6//! certificates, so one that fails that check is trusted on first use and
7//! pinned by its SHA-256 fingerprint in `tls_known_hosts`, like SSH host
8//! keys; a later, different certificate is refused. A system once verified
9//! by an authority is remembered as such, so it cannot later be passed off
10//! with a self-signed certificate.
11
12use 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
31/// The pin for a system verified by a certificate authority.
32const 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                // Many servers close without TLS's close_notify.
86                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
104/// Certificate authorities first, then trust on first use.
105struct Verifier {
106    webpki: Arc<WebPkiServerVerifier>,
107    host: String,
108    port: u16,
109    pins: PathBuf,
110    emitter: Emitter,
111    /// Why the certificate was refused, reported instead of rustls's error.
112    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
180/// "sha256:" and the certificate's SHA-256 in hex.
181fn 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
187/// Decide on a certificate with this `fingerprint`, which a certificate
188/// authority vouched for (`ca_ok`) or not, and update the pins. Returns a
189/// notice for the user when something new was trusted, or why the
190/// certificate is refused.
191pub 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        // Verified now: remember that, replacing any self-signed pin.
211        (_, 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        // Self-signed: trusted on first use, then held to its fingerprint.
246        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        // Another port is another system.
250        assert!(check_pin(&pins, "bbs.example", 993, "sha256:bb", false).unwrap().is_some());
251
252        // Verified by an authority: remembered, so self-signed is refused.
253        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}