security(windows): make the native-git TLS pins hold — and prove them — on Windows (#934) #939

Closed
brenno wants to merge 0 commits from security/934-native-git-windows-tls-pin into main
Owner

Closes #934 (split off from #926).

The question

Does native git honour the NetGuard pins on Windows — http.sslCAInfo (cert pin) and http.curloptResolve (host pin)? On macOS/Linux both are proven against a real HTTPS test server; on Windows the tests were skipped because git "could not connect" on the windows-2022 runner, so it stayed an open security question.

Findings

  1. The skip was a test-harness artifact, not a git limitation. The two real-server tests built their own POSIX-only environment (/dev/null, PATH only). On Windows, without SystemRoot the socket DLLs never load, so every connection failed regardless of the pin. The app's own _hardenedEnv already carries the Windows essentials (_carriedWindows), so the app was never affected — only the tests were.
  2. http.sslCAInfo genuinely is a paper measure on default Windows git. Git for Windows defaults to the schannel TLS backend, which ignores http.sslCAInfo outright (the installer even unsets it, to keep the Windows certificate store authoritative). Our self-signed cert pin would be silently dropped — git would validate against the system store instead of our anchor.
  3. http.curloptResolve and http.followRedirects are backend-independent (libcurl / git-level), so they hold on Windows unchanged.

Fix

  • On the pinned path only, force http.sslBackend=openssl on Windows (pinnedCertBackendConfig), so git validates against exactly the CA file we supply — the behaviour already proven on macOS/Linux. Scoped to the pin: a public-CA server sets no sslCAInfo, keeps schannel and the Windows store (including any corporate root).
  • Add hermeticGitEnv (mirrors _hardenedEnv's per-platform allowlist) and drive both real-server tests through it; un-skip them on Windows. The windows-2022 CI leg now verifies both pins empirically instead of assuming them.
  • Unit-test pinnedCertBackendConfig (both branches) so the Windows decision holds on any dev machine.

Verification

  • macOS: full make check green — behaviour off-Windows is unchanged (the openssl override is Windows-gated; both real-server tests still pass through the new hermetic env).
  • Windows: the empirical proof runs on the windows-2022 CI leg — both real-server tests now execute there. The curloptResolve test needs only git (preinstalled); the cert-pin test needs openssl and skips gracefully if it is absent, so no false red.

Docs: SECURITY_DESIGN §10 gains the Windows/schannel note; VERIFICATION §2 records the sub-question now under CI (the broader OQ-10 token byte-scan on Windows stays open).

Closes #934 (split off from #926). ## The question Does native git honour the NetGuard pins on Windows — `http.sslCAInfo` (cert pin) and `http.curloptResolve` (host pin)? On macOS/Linux both are proven against a real HTTPS test server; on Windows the tests were skipped because git "could not connect" on the windows-2022 runner, so it stayed an open security question. ## Findings 1. **The skip was a test-harness artifact, not a git limitation.** The two real-server tests built their own POSIX-only environment (`/dev/null`, `PATH` only). On Windows, without `SystemRoot` the socket DLLs never load, so *every* connection failed regardless of the pin. The app's own `_hardenedEnv` already carries the Windows essentials (`_carriedWindows`), so the app was never affected — only the tests were. 2. **`http.sslCAInfo` genuinely is a paper measure on default Windows git.** Git for Windows defaults to the schannel TLS backend, which ignores `http.sslCAInfo` outright (the installer even unsets it, to keep the Windows certificate store authoritative). Our self-signed cert pin would be silently dropped — git would validate against the system store instead of our anchor. 3. **`http.curloptResolve` and `http.followRedirects` are backend-independent** (libcurl / git-level), so they hold on Windows unchanged. ## Fix - On the pinned path only, force `http.sslBackend=openssl` on Windows (`pinnedCertBackendConfig`), so git validates against exactly the CA file we supply — the behaviour already proven on macOS/Linux. Scoped to the pin: a public-CA server sets no `sslCAInfo`, keeps schannel and the Windows store (including any corporate root). - Add `hermeticGitEnv` (mirrors `_hardenedEnv`'s per-platform allowlist) and drive both real-server tests through it; **un-skip them on Windows**. The windows-2022 CI leg now verifies both pins empirically instead of assuming them. - Unit-test `pinnedCertBackendConfig` (both branches) so the Windows decision holds on any dev machine. ## Verification - **macOS:** full `make check` green — behaviour off-Windows is unchanged (the openssl override is Windows-gated; both real-server tests still pass through the new hermetic env). - **Windows:** the empirical proof runs on the windows-2022 CI leg — both real-server tests now execute there. The `curloptResolve` test needs only git (preinstalled); the cert-pin test needs `openssl` and skips gracefully if it is absent, so no false red. Docs: SECURITY_DESIGN §10 gains the Windows/schannel note; VERIFICATION §2 records the sub-question now under CI (the broader OQ-10 token byte-scan on Windows stays open).
The native git path pins a self-signed server certificate by writing it out
and pointing `http.sslCAInfo` at it. But Git for Windows defaults to the
schannel TLS backend, which ignores `http.sslCAInfo` entirely (the installer
even unsets it, to keep the Windows certificate store authoritative). On
Windows the pin was therefore a paper measure: git would validate against the
system store instead of our anchor.

Force `http.sslBackend=openssl` on the pinned path (Windows only), so git
validates against exactly the CA file we supply — the behaviour already proven
with a real server on macOS/Linux. Scoped to the pinned connection: a public-CA
server sets no `sslCAInfo`, keeps schannel and the Windows store (incl. any
corporate root). The host-pin (`http.curloptResolve`) and redirect refusal are
libcurl/git-level and backend-independent, so they need nothing extra.

Split off from #926 as #934. Pure helper `pinnedCertBackendConfig` keeps the
decision unit-testable on any platform.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test(git): run the native-git pin tests on Windows CI (#934)
All checks were successful
scans / scans (pull_request) Successful in 3m18s
4d6491103f
The two real-server tests — that git honours http.curloptResolve (host pin) and
http.sslCAInfo (cert pin) — were skipped on Windows because git "could not
connect" on the windows-2022 runner. That was a harness artifact, not a git
limitation: the tests built their own POSIX-only env (`/dev/null`, PATH only),
and without SystemRoot the socket DLL never loads on Windows, so every
connection failed regardless of the pin.

Add `hermeticGitEnv`, mirroring NativeGitCli._hardenedEnv's per-platform
allowlist (SystemRoot etc., NUL for the global config), and drive both real-
server tests through it. Un-skip on Windows, and on the cert-pin test send the
same `http.sslBackend=openssl` the app now forces there (pinnedCertBackendConfig)
so the pin is honoured under schannel's replacement. The windows-2022 CI now
verifies both pins empirically instead of assuming them.

Also unit-tests pinnedCertBackendConfig (both branches) so the Windows decision
holds on any dev machine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
Owner

De wijzigingen staan al op main (cc359f2a, 4d649110) — de tak is volledig in main opgenomen, dus hier valt niets meer te mergen (vandaar het "try again later" op de merge-knop). Windows-CI groen: https://github.com/brennodewinter/Ocideck/actions/runs/30317415087. Sluiten; #934 is hiermee afgehandeld.

De wijzigingen staan al op main (`cc359f2a`, `4d649110`) — de tak is volledig in main opgenomen, dus hier valt niets meer te mergen (vandaar het "try again later" op de merge-knop). Windows-CI groen: https://github.com/brennodewinter/Ocideck/actions/runs/30317415087. Sluiten; #934 is hiermee afgehandeld.
brenno closed this pull request 2026-07-28 01:38:24 +00:00
All checks were successful
scans / scans (pull_request) Successful in 3m18s

Pull request closed

Sign in to join this conversation.
No description provided.