v0.8.5: Check-CA actually checks Windows now (follow-up to #13)

User on issue #13 reported that even after installing the CA (and
seeing it in the Windows cert manager UI), our 'Check CA' button still
said 'NOT trusted'. Root cause: is_ca_trusted() on Windows was just
returning false unconditionally — Check-CA has never worked on Windows.

Fix: is_trusted_windows() now shells out to certutil:
  certutil -user -store Root 'MasterHttpRelayVPN'
  certutil -store Root 'MasterHttpRelayVPN'

Checks both the user store (where our install_windows puts it by
default) and the machine store (fallback path when user-store install
is blocked). Requires certutil to print the cert name in stdout AND
exit 0 — belt-and-suspenders against locales where certutil exits 0
even on an empty match.

Also made the Check-CA UI message point users at the CA file path
for cross-device install — the same user reported their Android
V2rayNG client getting cert errors on our MITM-signed TLS leaves,
which is the expected 'the phone doesn't trust our CA' scenario. The
message now calls out the ca.crt path explicitly, and notes the
Android 7+ user-CA restriction (Firefox Android works, Chrome and
most apps don't trust user-installed CAs regardless).

Not addressed (by design):
- Replacing our CA keypair with Python-generated PEM fails to parse
  via rcgen. User tried this as a workaround before reporting. rcgen
  expects PKCS#8 PEM; Python's cryptography commonly emits PKCS#1
  ('BEGIN RSA PRIVATE KEY'). Even if parsing worked, mixing an
  external CA with our leaf-issuing code would break the key match.
  Users should stick with our generated CA — that's the supported
  flow. The Python cross-contamination experiment is expected to
  fail; we don't document it as supported.
This commit is contained in:
therealaleph
2026-04-22 18:30:18 +03:00
parent d49c494b20
commit 014c2a8cd1
4 changed files with 52 additions and 6 deletions
Generated
+1 -1
View File
@@ -1317,7 +1317,7 @@ dependencies = [
[[package]] [[package]]
name = "mhrv-rs" name = "mhrv-rs"
version = "0.8.4" version = "0.8.5"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "mhrv-rs" name = "mhrv-rs"
version = "0.8.4" version = "0.8.5"
edition = "2021" edition = "2021"
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting" description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
license = "MIT" license = "MIT"
+22 -3
View File
@@ -772,9 +772,28 @@ impl eframe::App for App {
ui.small(last_test_msg); ui.small(last_test_msg);
} }
match ca_trusted { match ca_trusted {
Some(true) => { ui.small("CA appears trusted."); }, Some(true) => {
Some(false) => { ui.small("CA is NOT trusted in the system store. Click 'Install CA' (may require admin)."); }, ui.small("CA appears trusted on this machine.");
None => {}, }
Some(false) => {
ui.small(
"CA is NOT trusted in the system store. Click 'Install CA' \
(may require admin). If you already installed it and this \
still says NO, you may be on an older build — v0.8.5+ \
checks the Windows store correctly.",
);
}
None => {}
}
if ca_trusted.is_some() {
let ca_path = data_dir::data_dir().join("ca").join("ca.crt");
ui.small(format!(
"For other devices (Android, other PCs) connecting through this proxy: \
copy {} and install as a trusted root on that device. On Android 7+ \
most apps ignore user-installed CAs — Firefox Android works; Chrome \
and many others don't.",
ca_path.display()
));
} }
ui.separator(); ui.separator();
+28 -1
View File
@@ -55,7 +55,7 @@ pub fn is_ca_trusted(path: &Path) -> bool {
match std::env::consts::OS { match std::env::consts::OS {
"macos" => is_trusted_macos(), "macos" => is_trusted_macos(),
"linux" => is_trusted_linux(), "linux" => is_trusted_linux(),
"windows" => false, "windows" => is_trusted_windows(),
_ => false, _ => false,
} }
} }
@@ -303,6 +303,33 @@ fn is_trusted_linux() -> bool {
// ---------- Windows ---------- // ---------- Windows ----------
/// Check whether our CA is present in the Windows Trusted Root store.
/// Looks in both the user store (no admin required to install) and the
/// machine store. Returns true if `certutil -store ... MasterHttpRelayVPN`
/// finds a match. Issue #13 follow-up: previously this always returned
/// false on Windows, so the Check-CA button was misleading users into
/// reinstalling a cert that was already trusted.
fn is_trusted_windows() -> bool {
// `certutil -user -store Root <name>` prints the matching cert entries
// on success (stdout), and exits with a non-zero code plus a "Not
// found" message if nothing matches. We also check stdout for the
// cert name because certutil in some locales returns 0 even on no-
// match, just with empty output.
for args in [
vec!["-user", "-store", "Root", CERT_NAME],
vec!["-store", "Root", CERT_NAME],
] {
let out = Command::new("certutil").args(&args).output();
if let Ok(o) = out {
let stdout = String::from_utf8_lossy(&o.stdout);
if o.status.success() && stdout.to_ascii_lowercase().contains(&CERT_NAME.to_ascii_lowercase()) {
return true;
}
}
}
false
}
fn install_windows(cert_path: &str) -> bool { fn install_windows(cert_path: &str) -> bool {
// Per-user Root store (no admin required). // Per-user Root store (no admin required).
let res = Command::new("certutil") let res = Command::new("certutil")