Skip to content

Commit 1c92657

Browse files
authored
feat(desktop): OS trust store and mTLS client certs for native HTTP (#1225)
* feat(desktop): OS trust store and mTLS client certs for native HTTP The desktop app's native Rust HTTP client (guarded_http_client) trusted only the bundled Mozilla roots and could not present a client certificate, so remote fetches (tile/URL resolution, OGC GetCapabilities) to enterprise endpoints behind a private CA or mutual TLS failed, while the WebView fetch path succeeded via the OS trust store and an interactive cert prompt. - Trust the OS/system certificate store in addition to the bundled roots (rustls-tls-native-roots), so enterprise-CA-signed servers work with no configuration. - Present a client certificate for mutual TLS when configured via environment variables: GEOLIBRE_HTTP_CLIENT_CERT (PEM or PKCS#12), an optional GEOLIBRE_HTTP_CLIENT_CERT_PASSWORD, and GEOLIBRE_HTTP_CA_CERT for a private CA. PEM identities use the default rustls backend; PKCS#12 (with passphrase) switches that one client to the platform native-tls backend. The interactive OS certificate prompt the WebView shows is not reachable from reqwest, so the native path uses this config-based mechanism instead. Verified end to end against a local mTLS server: both the PEM (rustls) and PKCS#12-with-passphrase (native-tls) client-cert paths complete the handshake, a client without the cert is rejected, and a wrong passphrase fails to load. Refs #1220 * perf(desktop): cache the guarded HTTP client; error on stray mTLS passphrase Address review feedback on the native mTLS change: - Build the SSRF-guarded HTTP client once and cache it behind a OnceLock instead of rebuilding it (re-reading and re-parsing the CA bundle and client certificate from disk) on every native fetch. Callers now set their own per-request deadline with RequestBuilder::timeout(), so the shared client keeps its connection pool and parsed TLS material across fetch and URL-resolve calls. - Surface GEOLIBRE_HTTP_CLIENT_CERT_PASSWORD set without GEOLIBRE_HTTP_CLIENT_CERT as a clear error rather than silently discarding the passphrase. - Note in guarded_http_client that the SSRF guard is applied independent of the TLS backend, so it holds on the native-tls (PKCS#12) path too. Adds a unit test for the stray-passphrase guard. * fix(desktop): treat an empty mTLS passphrase as unset Env interpolation in Docker/K8s/.env tooling (e.g. PASSWORD=${SECRET:-}) commonly yields an empty string rather than leaving the variable unset. An empty GEOLIBRE_HTTP_CLIENT_CERT_PASSWORD now counts as no passphrase, so it neither forces the PKCS#12 code path for a PEM certificate nor trips the stray-passphrase error. * fix(desktop): treat empty cert paths as unset; error on non-UTF-8 passphrase More review follow-ups on the native mTLS config: - Filter set-but-empty GEOLIBRE_HTTP_CA_CERT and GEOLIBRE_HTTP_CLIENT_CERT the same way as the passphrase, so ${SECRET:-}-style empty values are treated as unset instead of read as the path "" (which would fail on fs::read and, since the guarded client is cached, permanently break every native fetch). - Surface a non-UTF-8 GEOLIBRE_HTTP_CLIENT_CERT_PASSWORD as a clear error rather than silently dropping it, since the PKCS#12 loader takes a &str passphrase. - Document that the configured client certificate is held on the shared native HTTP client and is presented to any host that requests one during the TLS handshake, so it should only be configured when the app's hosts are trusted.
1 parent c4617c7 commit 1c92657

4 files changed

Lines changed: 383 additions & 14 deletions

File tree

apps/geolibre-desktop/src-tauri/Cargo.lock

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

apps/geolibre-desktop/src-tauri/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ tauri-plugin-opener = "2"
3939
tauri-plugin-persisted-scope = "2"
4040
duckdb = { version = "1.10504.0", features = ["bundled", "parquet"], optional = true }
4141
chrono = { version = "0.4", default-features = false, features = ["std"] }
42-
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
42+
# rustls-tls stays the default backend for every request. rustls-tls-native-roots
43+
# also trusts the OS/system certificate store (enterprise CAs), and native-tls is
44+
# used only on the opt-in mTLS path for PKCS#12 client certificates (issue #1220).
45+
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls", "rustls-tls-native-roots", "native-tls"] }
4346
flate2 = "1"
4447
tar = "0.4"
4548
zip = { version = "8", default-features = false, features = ["deflate"] }

0 commit comments

Comments
 (0)