Skip to content

Commit e700b4b

Browse files
committed
v0.8.2: CA trust — Firefox enterprise_roots + Chrome ~/.pki/nssdb (follow-up to #11)
After v0.8.1 fixed the leaf cert extensions, users reported "still broken" — specifically Firefox showing: "Software is Preventing Firefox From Safely Connecting to This Site. drive.google.com ... This issue is caused by MasterHttpRelayVPN" for HSTS-preloaded sites. That error is Firefox's "MITM detected AND issuing CA isn't in my trust store" path combined with HSTS blocking the normal override button — so users were stuck with no workaround. Real root cause of the "still broken" reports: the CA was making it into the OS trust store (Windows cert store / update-ca-certificates on Linux) but NOT into the browser-specific trust stores that Firefox and Chrome use on every OS. Three additions: 1. Firefox: . For every Firefox profile we find, we now write this pref to the profile's user.js. It tells Firefox to trust the OS CA store, so our already-successful system-level install automatically covers Firefox on next startup. Critical on Windows (NSS certutil isn't on PATH there, so the certutil-based Firefox install never worked). Idempotent — checks for existing pref before writing and leaves a non-matching user value alone. 2. Chrome/Chromium on Linux: install into ~/.pki/nssdb. Linux Chrome uses its own shared NSS DB, independent of both the OS store (populated by update-ca-certificates) AND Firefox's per-profile NSS. Without this, users installed the CA via run.sh, Chrome still refused every HTTPS site, and they spiraled trying to re-install the CA. We now also initialize that DB with if it doesn't exist yet. 3. Refactored the NSS-install path so Firefox and Chrome share a single install_nss_in_dir() helper. Renamed the top-level entry from install_firefox_nss to install_nss_stores to match scope. Locally verified the cert itself is fine — openssl x509 -text shows Version 3, SAN, KeyUsage (critical), ExtendedKeyUsage, and passes. So the leaf is correct; what was failing was the trust-chain validation inside the specific browser because our CA wasn't in THAT browser's trust DB. Upgrade path: download v0.8.2 and run the launcher or `./mhrv-rs --install-cert`. Restart Firefox/Chrome after install — Firefox needs the restart to re-read user.js.
1 parent 038ad8c commit e700b4b

3 files changed

Lines changed: 164 additions & 53 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mhrv-rs"
3-
version = "0.8.1"
3+
version = "0.8.2"
44
edition = "2021"
55
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
66
license = "MIT"

src/cert_installer.rs

Lines changed: 162 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ pub fn install_ca(path: &Path) -> Result<(), InstallError> {
3232
other => return Err(InstallError::Unsupported(other.to_string())),
3333
};
3434

35-
// Best-effort: also try to install into Firefox NSS stores if certutil
36-
// is available. Firefox maintains its own trust store separate from the OS.
37-
install_firefox_nss(&path_s);
35+
// Best-effort: also install into NSS stores if `certutil` is available.
36+
// Both Firefox AND Chrome/Chromium on Linux maintain NSS databases that
37+
// are independent of the OS trust store — which is why running
38+
// update-ca-certificates alone wasn't enough for a lot of users
39+
// (issue #11 on Linux was this).
40+
install_nss_stores(&path_s);
3841

3942
if ok {
4043
Ok(())
@@ -325,67 +328,163 @@ fn install_windows(cert_path: &str) -> bool {
325328
false
326329
}
327330

328-
// ---------- Firefox (NSS) ----------
329-
330-
/// Best-effort install of the CA into all discovered Firefox profiles.
331-
/// Silently no-ops if `certutil` (from libnss3-tools) is not available.
332-
/// Firefox must be closed during install for changes to take effect.
333-
fn install_firefox_nss(cert_path: &str) {
334-
// Check if certutil exists at all.
335-
if Command::new("certutil")
336-
.arg("--help")
337-
.output()
338-
.ok()
339-
.map(|o| {
340-
// macOS has a different certutil (built-in) that doesn't support -d.
341-
// Look for NSS-specific flags in the help output.
342-
String::from_utf8_lossy(&o.stderr).contains("-d")
343-
|| String::from_utf8_lossy(&o.stdout).contains("-d")
344-
})
345-
.unwrap_or(false)
346-
== false
347-
{
331+
// ---------- NSS (Firefox + Chrome/Chromium on Linux) ----------
332+
333+
/// Best-effort install of the CA into all discovered NSS stores:
334+
/// 1. Every Firefox profile (each has its own cert9.db).
335+
/// 2. On Linux, the shared Chrome/Chromium NSS DB at ~/.pki/nssdb —
336+
/// this is the one update-ca-certificates does NOT populate, and
337+
/// missing it was the real blocker for Chrome users who'd installed
338+
/// the OS-level CA and still got cert errors (part of issue #11).
339+
/// Silently no-ops if `certutil` (from libnss3-tools) isn't on PATH.
340+
/// Browsers must be closed during install for changes to take effect.
341+
fn install_nss_stores(cert_path: &str) {
342+
// First, try to make Firefox pick up the OS-level CA automatically by
343+
// flipping the `security.enterprise_roots.enabled` pref in user.js of
344+
// every Firefox profile we find. This is the cleanest cross-platform
345+
// fix because it doesn't depend on whether NSS certutil is installed
346+
// — Firefox just starts trusting whatever the OS trusts. Especially
347+
// important on Windows where NSS certutil isn't on PATH.
348+
enable_firefox_enterprise_roots();
349+
350+
if !has_nss_certutil() {
348351
tracing::debug!(
349-
"NSS certutil not found — Firefox users must import ca.crt manually \
350-
via Settings -> Privacy & Security -> Certificates."
352+
"NSS certutil not found — Firefox will still trust the CA via the \
353+
`security.enterprise_roots.enabled` user.js pref (flipped above). \
354+
For Chrome/Chromium on Linux, install `libnss3-tools` (Debian/Ubuntu) \
355+
or `nss-tools` (Fedora/RHEL), or import ca.crt manually via \
356+
chrome://settings/certificates → Authorities."
351357
);
352358
return;
353359
}
354360

355-
let profiles = firefox_profile_dirs();
356-
if profiles.is_empty() {
357-
tracing::debug!("no Firefox profiles found");
358-
return;
359-
}
360-
361361
let mut ok = 0;
362-
for p in &profiles {
363-
if install_nss_in_profile(p, cert_path) {
362+
let mut tried = 0;
363+
364+
// 1. Firefox profiles.
365+
for p in firefox_profile_dirs() {
366+
tried += 1;
367+
if install_nss_in_profile(&p, cert_path) {
364368
ok += 1;
365369
}
366370
}
371+
372+
// 2. Chrome/Chromium shared NSS DB (Linux only).
373+
#[cfg(target_os = "linux")]
374+
{
375+
if let Some(nssdb) = chrome_nssdb_path() {
376+
// Ensure the DB exists. certutil -N creates an empty cert9.db in
377+
// the directory if none is there. An empty passphrase is fine
378+
// for a user-local DB.
379+
let dir_arg = format!("sql:{}", nssdb.display());
380+
if !nssdb.join("cert9.db").exists() && !nssdb.join("cert8.db").exists() {
381+
let _ = std::fs::create_dir_all(&nssdb);
382+
let _ = Command::new("certutil")
383+
.args(["-N", "-d", &dir_arg, "--empty-password"])
384+
.output();
385+
}
386+
tried += 1;
387+
if install_nss_in_dir(&dir_arg, cert_path) {
388+
ok += 1;
389+
tracing::info!(
390+
"CA installed in Chrome/Chromium NSS DB: {}",
391+
nssdb.display()
392+
);
393+
}
394+
}
395+
}
396+
367397
if ok > 0 {
368-
tracing::info!("CA installed in {} Firefox profile(s).", ok);
369-
} else {
370-
tracing::debug!(
371-
"No Firefox profiles updated. If Firefox wasn't running, try installing manually."
398+
tracing::info!("CA installed in {}/{} NSS store(s).", ok, tried);
399+
} else if tried > 0 {
400+
tracing::warn!(
401+
"NSS install: 0/{} stores updated. If Firefox/Chrome was running, close \
402+
them and retry. Otherwise, import ca.crt manually via browser settings.",
403+
tried
372404
);
373405
}
374406
}
375407

376-
fn install_nss_in_profile(profile: &Path, cert_path: &str) -> bool {
377-
let prefix = if profile.join("cert9.db").exists() {
378-
"sql:"
379-
} else if profile.join("cert8.db").exists() {
380-
""
381-
} else {
382-
return false;
383-
};
384-
let dir_arg = format!("{}{}", prefix, profile.display());
408+
/// Write `user_pref("security.enterprise_roots.enabled", true);` to every
409+
/// discovered Firefox profile's user.js. This makes Firefox trust the OS
410+
/// trust store on next startup — so our already-successful system-level
411+
/// CA install automatically propagates. Critical on Windows where Firefox
412+
/// keeps its own NSS DB independent of Windows cert store, and NSS
413+
/// certutil isn't typically installed so the certutil-based path doesn't
414+
/// fire there.
415+
///
416+
/// Existing user.js entries for other prefs are preserved by appending
417+
/// rather than truncating. Idempotent.
418+
fn enable_firefox_enterprise_roots() {
419+
const PREF: &str = r#"user_pref("security.enterprise_roots.enabled", true);"#;
420+
let mut touched = 0;
421+
for profile in firefox_profile_dirs() {
422+
let user_js = profile.join("user.js");
423+
let existing = std::fs::read_to_string(&user_js).unwrap_or_default();
424+
if existing.contains("security.enterprise_roots.enabled") {
425+
// Already set by us or the user. Replace-or-keep: if they set it
426+
// to false we leave their choice alone. If it's already our line
427+
// verbatim, nothing to do.
428+
if existing.contains(PREF) {
429+
continue;
430+
}
431+
// Different value present — don't overwrite.
432+
tracing::debug!(
433+
"firefox profile {} already has a different enterprise_roots pref; leaving alone",
434+
profile.display()
435+
);
436+
continue;
437+
}
438+
let mut out = existing;
439+
if !out.is_empty() && !out.ends_with('\n') {
440+
out.push('\n');
441+
}
442+
out.push_str(PREF);
443+
out.push('\n');
444+
if let Err(e) = std::fs::write(&user_js, out) {
445+
tracing::debug!(
446+
"firefox profile {}: user.js write failed: {}",
447+
profile.display(),
448+
e
449+
);
450+
continue;
451+
}
452+
touched += 1;
453+
}
454+
if touched > 0 {
455+
tracing::info!(
456+
"enabled Firefox enterprise_roots in {} profile(s) — restart Firefox for it to take effect",
457+
touched
458+
);
459+
}
460+
}
461+
462+
fn has_nss_certutil() -> bool {
463+
Command::new("certutil")
464+
.arg("--help")
465+
.output()
466+
.ok()
467+
.map(|o| {
468+
// macOS has a different certutil built-in that doesn't support -d.
469+
// NSS-specific help output mentions the -d / -n flags.
470+
String::from_utf8_lossy(&o.stderr).contains("-d")
471+
|| String::from_utf8_lossy(&o.stdout).contains("-d")
472+
})
473+
.unwrap_or(false)
474+
}
385475

476+
#[cfg(target_os = "linux")]
477+
fn chrome_nssdb_path() -> Option<std::path::PathBuf> {
478+
let home = std::env::var("HOME").ok()?;
479+
Some(std::path::PathBuf::from(format!("{}/.pki/nssdb", home)))
480+
}
481+
482+
/// Install into a given sql: or legacy NSS DB path. Factored out so both
483+
/// Firefox-per-profile and Chrome-shared paths share one code path.
484+
fn install_nss_in_dir(dir_arg: &str, cert_path: &str) -> bool {
386485
// Delete any stale entry first (ignore errors).
387486
let _ = Command::new("certutil")
388-
.args(["-D", "-n", CERT_NAME, "-d", &dir_arg])
487+
.args(["-D", "-n", CERT_NAME, "-d", dir_arg])
389488
.output();
390489

391490
let res = Command::new("certutil")
@@ -396,31 +495,43 @@ fn install_nss_in_profile(profile: &Path, cert_path: &str) -> bool {
396495
"-t",
397496
"C,,",
398497
"-d",
399-
&dir_arg,
498+
dir_arg,
400499
"-i",
401500
cert_path,
402501
])
403502
.output();
404503
match res {
405504
Ok(o) if o.status.success() => {
406-
tracing::debug!("NSS install ok: {}", profile.display());
505+
tracing::debug!("NSS install ok: {}", dir_arg);
407506
true
408507
}
409508
Ok(o) => {
410509
tracing::debug!(
411510
"NSS install failed for {}: {}",
412-
profile.display(),
511+
dir_arg,
413512
String::from_utf8_lossy(&o.stderr).trim()
414513
);
415514
false
416515
}
417516
Err(e) => {
418-
tracing::debug!("NSS certutil exec failed for {}: {}", profile.display(), e);
517+
tracing::debug!("NSS certutil exec failed for {}: {}", dir_arg, e);
419518
false
420519
}
421520
}
422521
}
423522

523+
fn install_nss_in_profile(profile: &Path, cert_path: &str) -> bool {
524+
let prefix = if profile.join("cert9.db").exists() {
525+
"sql:"
526+
} else if profile.join("cert8.db").exists() {
527+
""
528+
} else {
529+
return false;
530+
};
531+
let dir_arg = format!("{}{}", prefix, profile.display());
532+
install_nss_in_dir(&dir_arg, cert_path)
533+
}
534+
424535
fn firefox_profile_dirs() -> Vec<std::path::PathBuf> {
425536
use std::path::PathBuf;
426537
let mut roots: Vec<PathBuf> = Vec::new();

0 commit comments

Comments
 (0)