anvilsign in

collin/browser-terminal-extension

1//! Self-signed TLS for loopback.
2//!
3//! Firefox's HTTPS-Only Mode rewrites `ws://` to `wss://`, including for
4//! loopback. Rather than asking people to weaken a browser security setting so
5//! our daemon can run, we just speak TLS.
6//!
7//! The certificate is generated once, stored in the config dir with the key at
8//! 0600, and covers `127.0.0.1`, `::1` and `localhost`. It is self-signed, so
9//! the browser needs a one-time exception — see `serve` output.
10
11use std::fs;
12use std::io::Write;
13use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
14use std::path::Path;
15use std::sync::Arc;
16
17use tokio_rustls::TlsAcceptor;
18use tokio_rustls::rustls::ServerConfig;
19use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer};
20
21pub const CERT_FILE: &str = "cert.pem";
22pub const KEY_FILE: &str = "key.pem";
23
24/// Regenerate when the cert is within this of expiry, so a long-lived install
25/// doesn't silently start failing.
26const VALIDITY_DAYS: i64 = 3650;
27
28pub struct Identity {
29 pub cert_pem: String,
30 pub key_pem: String,
31 /// SHA-256 of the DER certificate, colon-separated — the same fingerprint
32 /// the browser shows, so a user can verify they're trusting our cert.
33 pub fingerprint: String,
34}
35
36fn fingerprint(der: &[u8]) -> String {
37 // Avoid pulling in a hash crate for one call; rcgen already depends on ring.
38 let digest = ring_sha256(der);
39 digest
40 .iter()
41 .map(|b| format!("{b:02X}"))
42 .collect::<Vec<_>>()
43 .join(":")
44}
45
46fn ring_sha256(data: &[u8]) -> Vec<u8> {
47 use sha2::{Digest, Sha256};
48 Sha256::digest(data).to_vec()
49}
50
51/// Load the existing identity, or generate one on first run.
52pub fn load_or_create(dir: &Path) -> std::io::Result<Identity> {
53 let cert_path = dir.join(CERT_FILE);
54 let key_path = dir.join(KEY_FILE);
55
56 if cert_path.exists() && key_path.exists() {
57 let mode = fs::metadata(&key_path)?.permissions().mode() & 0o777;
58 if mode & 0o077 != 0 {
59 return Err(std::io::Error::other(format!(
60 "{} has mode {mode:04o}; expected 0600. Fix with: chmod 600 {}",
61 key_path.display(),
62 key_path.display()
63 )));
64 }
65 let cert_pem = fs::read_to_string(&cert_path)?;
66 let key_pem = fs::read_to_string(&key_path)?;
67 let der = first_cert_der(&cert_pem)?;
68 return Ok(Identity {
69 fingerprint: fingerprint(&der),
70 cert_pem,
71 key_pem,
72 });
73 }
74
75 generate(dir)
76}
77
78pub fn generate(dir: &Path) -> std::io::Result<Identity> {
79 fs::create_dir_all(dir)?;
80 fs::set_permissions(dir, fs::Permissions::from_mode(0o700))?;
81
82 let mut params = rcgen::CertificateParams::new(vec![
83 "localhost".to_string(),
84 "127.0.0.1".to_string(),
85 "::1".to_string(),
86 ])
87 .map_err(|e| std::io::Error::other(e.to_string()))?;
88
89 params.distinguished_name = {
90 let mut dn = rcgen::DistinguishedName::new();
91 dn.push(rcgen::DnType::CommonName, "termbridge (local)");
92 dn.push(rcgen::DnType::OrganizationName, "termbridge");
93 dn
94 };
95 params.not_before = rcgen::date_time_ymd(2020, 1, 1);
96 params.not_after = rcgen::date_time_ymd(2025 + (VALIDITY_DAYS / 365) as i32, 1, 1);
97 // A leaf, not a CA: this cert can only ever vouch for this host, so
98 // trusting it cannot be leveraged to impersonate anything else.
99 params.is_ca = rcgen::IsCa::ExplicitNoCa;
100
101 let key = rcgen::KeyPair::generate().map_err(|e| std::io::Error::other(e.to_string()))?;
102 let cert = params
103 .self_signed(&key)
104 .map_err(|e| std::io::Error::other(e.to_string()))?;
105
106 let cert_pem = cert.pem();
107 let key_pem = key.serialize_pem();
108
109 fs::write(dir.join(CERT_FILE), &cert_pem)?;
110 fs::set_permissions(dir.join(CERT_FILE), fs::Permissions::from_mode(0o644))?;
111
112 // The private key gets the same treatment as the auth token.
113 let key_path = dir.join(KEY_FILE);
114 let mut f = fs::OpenOptions::new()
115 .write(true)
116 .create(true)
117 .truncate(true)
118 .mode(0o600)
119 .open(&key_path)?;
120 f.write_all(key_pem.as_bytes())?;
121 f.sync_all()?;
122 fs::set_permissions(&key_path, fs::Permissions::from_mode(0o600))?;
123
124 let der = cert.der().to_vec();
125 Ok(Identity {
126 fingerprint: fingerprint(&der),
127 cert_pem,
128 key_pem,
129 })
130}
131
132fn first_cert_der(pem: &str) -> std::io::Result<Vec<u8>> {
133 let mut cursor = std::io::Cursor::new(pem.as_bytes());
134 let certs: Vec<CertificateDer> = rustls_pemfile_certs(&mut cursor)?;
135 certs
136 .first()
137 .map(|c| c.as_ref().to_vec())
138 .ok_or_else(|| std::io::Error::other("no certificate in cert.pem"))
139}
140
141/// Minimal PEM decoder so we don't add rustls-pemfile for two call sites.
142fn rustls_pemfile_certs(
143 r: &mut std::io::Cursor<&[u8]>,
144) -> std::io::Result<Vec<CertificateDer<'static>>> {
145 let text = String::from_utf8_lossy(*r.get_ref()).to_string();
146 let mut out = Vec::new();
147 for block in text.split("-----BEGIN CERTIFICATE-----").skip(1) {
148 let Some(body) = block.split("-----END CERTIFICATE-----").next() else {
149 continue;
150 };
151 let b64: String = body.chars().filter(|c| !c.is_whitespace()).collect();
152 out.push(CertificateDer::from(base64_decode(&b64)?));
153 }
154 Ok(out)
155}
156
157fn base64_decode(s: &str) -> std::io::Result<Vec<u8>> {
158 const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
159 let mut rev = [255u8; 256];
160 for (i, c) in TABLE.iter().enumerate() {
161 rev[*c as usize] = i as u8;
162 }
163 let mut out = Vec::new();
164 let mut acc: u32 = 0;
165 let mut bits = 0;
166 for c in s.bytes() {
167 if c == b'=' {
168 break;
169 }
170 let v = rev[c as usize];
171 if v == 255 {
172 return Err(std::io::Error::other("invalid base64 in PEM"));
173 }
174 acc = (acc << 6) | v as u32;
175 bits += 6;
176 if bits >= 8 {
177 bits -= 8;
178 out.push((acc >> bits) as u8);
179 }
180 }
181 Ok(out)
182}
183
184pub fn acceptor(identity: &Identity) -> std::io::Result<TlsAcceptor> {
185 let mut cert_cursor = std::io::Cursor::new(identity.cert_pem.as_bytes());
186 let certs = rustls_pemfile_certs(&mut cert_cursor)?;
187
188 let key = parse_private_key(&identity.key_pem)?;
189
190 let config = ServerConfig::builder()
191 .with_no_client_auth()
192 .with_single_cert(certs, key)
193 .map_err(|e| std::io::Error::other(e.to_string()))?;
194
195 Ok(TlsAcceptor::from(Arc::new(config)))
196}
197
198fn parse_private_key(pem: &str) -> std::io::Result<PrivateKeyDer<'static>> {
199 for (begin, end) in [
200 ("-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----"),
201 (
202 "-----BEGIN EC PRIVATE KEY-----",
203 "-----END EC PRIVATE KEY-----",
204 ),
205 (
206 "-----BEGIN RSA PRIVATE KEY-----",
207 "-----END RSA PRIVATE KEY-----",
208 ),
209 ] {
210 if let Some(rest) = pem.split(begin).nth(1) {
211 if let Some(body) = rest.split(end).next() {
212 let b64: String = body.chars().filter(|c| !c.is_whitespace()).collect();
213 let der = base64_decode(&b64)?;
214 return Ok(match begin {
215 "-----BEGIN EC PRIVATE KEY-----" => PrivateKeyDer::Sec1(der.into()),
216 "-----BEGIN RSA PRIVATE KEY-----" => PrivateKeyDer::Pkcs1(der.into()),
217 _ => PrivateKeyDer::Pkcs8(der.into()),
218 });
219 }
220 }
221 }
222 Err(std::io::Error::other("no private key found in key.pem"))
223}