Skip to content

Commit 27cda07

Browse files
committed
v0.8.1: fix MITM leaf cert extensions — HTTPS through the proxy now works (closes #11)
Multiple users reported the same thing (issue #11): they trusted the CA, then re-installed it, then deleted and re-generated it, and still every HTTPS site through the proxy failed in the browser. The python version of the same project doesn't have the issue. Root cause: rcgen's CertificateParams::default() produces a minimum-viable x509 cert that does NOT carry: - ExtendedKeyUsage extension with id-kp-serverAuth - KeyUsage extension with digitalSignature + keyEncipherment Modern Chrome / Firefox / Edge / Safari all reject TLS server leaves without those. The CA trust bit didn't matter — the browser's chain validator rejected the leaf itself with NET::ERR_CERT_INVALID before ever consulting the trust store. So 'reinstall the CA' was powerless to help. Fix in src/mitm.rs::issue_leaf: - Set params.extended_key_usages = [ServerAuth]. - Set params.key_usages = [DigitalSignature, KeyEncipherment]. - Backdate not_before by 5 min to absorb clock skew between the MITM process and a slightly-fast client clock. Same fix in the CA's own not_before. Also added src/mitm.rs::tests::leaf_has_serverauth_eku_and_key_usage as a permanent regression guard — it parses the DER with x509-parser and asserts the three extensions are present. Added x509-parser to dev-dependencies (already in the tree transitively via rcgen). Upgrade path for users affected by #11: download v0.8.1, run it. No CA reinstall required — the CA cert itself was fine, only the per- site leaves were broken. Verified end-to-end locally: curl --cacert <ca.crt> -x http://127.0.0.1:... https://httpbin.org/ip curl --cacert <ca.crt> -x socks5h://127.0.0.1:... https://httpbin.org/ip Both return JSON without cert errors, through the Apps Script relay path. 37 unit tests pass.
1 parent 501c03a commit 27cda07

3 files changed

Lines changed: 70 additions & 6 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mhrv-rs"
3-
version = "0.8.0"
3+
version = "0.8.1"
44
edition = "2021"
55
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
66
license = "MIT"
@@ -62,6 +62,10 @@ eframe = { version = "0.28", default-features = false, features = [
6262
[target.'cfg(unix)'.dependencies]
6363
libc = "0.2"
6464

65+
[dev-dependencies]
66+
# Used in mitm tests to sanity-check the cert extensions we emit.
67+
x509-parser = "0.16"
68+
6569
[profile.release]
6670
panic = "abort"
6771
codegen-units = 1

src/mitm.rs

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ use std::path::{Path, PathBuf};
33
use std::sync::Arc;
44

55
use rcgen::{
6-
BasicConstraints, Certificate, CertificateParams, DistinguishedName, DnType, IsCa, KeyPair,
7-
KeyUsagePurpose, SanType,
6+
BasicConstraints, Certificate, CertificateParams, DistinguishedName, DnType,
7+
ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, SanType,
88
};
99
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
1010
use rustls::ServerConfig;
@@ -104,7 +104,7 @@ impl MitmCertManager {
104104
KeyUsagePurpose::CrlSign,
105105
];
106106
let now = time::OffsetDateTime::now_utc();
107-
params.not_before = now;
107+
params.not_before = now - time::Duration::minutes(5);
108108
params.not_after = now + time::Duration::days(3650);
109109

110110
let key_pair = KeyPair::generate()?;
@@ -162,8 +162,25 @@ impl MitmCertManager {
162162
MitmError::Invalid(format!("bad dns name '{}': {}", domain, e))
163163
})?;
164164
params.subject_alt_names.push(SanType::DnsName(dns_name));
165+
166+
// Modern browsers (Chrome/Firefox, all current versions) reject TLS
167+
// leaves that don't carry:
168+
// - ExtendedKeyUsage: serverAuth → NET::ERR_CERT_INVALID otherwise
169+
// - KeyUsage: digitalSignature + keyEncipherment
170+
// rcgen's `CertificateParams::default()` doesn't set these — we have
171+
// to add them explicitly. Skipping this was the root cause of issue #11
172+
// where users reinstalled the trusted CA dozens of times and browsers
173+
// still refused to load HTTPS sites through the proxy.
174+
params.key_usages = vec![
175+
KeyUsagePurpose::DigitalSignature,
176+
KeyUsagePurpose::KeyEncipherment,
177+
];
178+
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
179+
180+
// Backdate not_before by 5 min to absorb clock skew between the
181+
// MITM process and the browser / system clock.
165182
let now = time::OffsetDateTime::now_utc();
166-
params.not_before = now;
183+
params.not_before = now - time::Duration::minutes(5);
167184
params.not_after = now + time::Duration::days(365);
168185

169186
let leaf_key = KeyPair::generate()?;
@@ -198,6 +215,48 @@ mod tests {
198215
let _ = std::fs::remove_dir_all(&tmp);
199216
}
200217

218+
#[test]
219+
fn leaf_has_serverauth_eku_and_key_usage() {
220+
// Regression guard for issue #11. rcgen's CertificateParams::default()
221+
// doesn't set these extensions; without them modern Chrome/Firefox
222+
// reject every leaf with NET::ERR_CERT_INVALID even after the CA is
223+
// trusted. Verified by spot-checking the DER with x509-parser.
224+
use x509_parser::prelude::*;
225+
226+
init_crypto();
227+
let tmp = tempdir();
228+
let m = MitmCertManager::new_in(&tmp).unwrap();
229+
let (leaf_der, _) = m.issue_leaf("example.com").unwrap();
230+
let (_, parsed) = X509Certificate::from_der(&leaf_der).unwrap();
231+
232+
// ExtendedKeyUsage: serverAuth present.
233+
let eku = parsed
234+
.extended_key_usage()
235+
.expect("eku extension lookup")
236+
.expect("eku extension present");
237+
assert!(eku.value.server_auth, "leaf must have serverAuth EKU");
238+
239+
// KeyUsage: digitalSignature + keyEncipherment present.
240+
let ku = parsed
241+
.key_usage()
242+
.expect("key_usage extension lookup")
243+
.expect("key_usage extension present");
244+
assert!(ku.value.digital_signature(), "leaf must have digitalSignature KU");
245+
assert!(ku.value.key_encipherment(), "leaf must have keyEncipherment KU");
246+
247+
// SAN has the domain we asked for.
248+
let san = parsed
249+
.subject_alternative_name()
250+
.expect("san extension lookup")
251+
.expect("san extension present");
252+
let has_name = san.value.general_names.iter().any(|n| {
253+
matches!(n, GeneralName::DNSName(s) if *s == "example.com")
254+
});
255+
assert!(has_name, "leaf SAN must contain example.com");
256+
257+
let _ = std::fs::remove_dir_all(&tmp);
258+
}
259+
201260
#[test]
202261
fn issues_different_certs_per_domain() {
203262
init_crypto();

0 commit comments

Comments
 (0)