Skip to content

fix(windows): read the Netskope chain from the certificate store and keep the intermediate - #117

Merged
aberoham merged 3 commits into
aberoham:mainfrom
joshblease:fix/windows-netskope-cert-store
Oct 26, 2026
Merged

aberoham merged 3 commits into
aberoham:mainfrom
joshblease:fix/windows-netskope-cert-store

Conversation

@joshblease

Copy link
Copy Markdown
Contributor

Problem

A colleague on Windows with the hardened Netskope client (only nscacert.pem.enc on disk) was told by fumitm_windows.py to export the root by hand. After doing so and running --fix, the Google Cloud SDK installer failed with:

requests.exceptions.SSLError: ... certificate verify failed: CA cert does not include key usage extension (_ssl.c:1082)

Three things were wrong, confirmed on the machine:

  1. The Netskope root CA has no Key Usage extension. The tenant intermediate (ca.<tenant>.goskope.com) has one. Python 3.13+ enables VERIFY_X509_STRICT by default, so any chain that reaches the root is rejected. With the intermediate in the bundle, VERIFY_X509_PARTIAL_CHAIN stops chain building there and the root is never examined. Reproduced against dl.google.com: a root-only bundle fails on 3.13 and 3.14 and passes on 3.12; root plus intermediate passes on all three.
  2. The install-mode duplicate check never worked on Windows. It unlinked a NamedTemporaryFile while it was still open, which Windows refuses, so every --fix appended another copy of the root. The user's python\ca-bundle.pem held three identical roots.
  3. The bundle had no system roots. It had been seeded from a hand-made REQUESTS_CA_BUNDLE file holding only the root, so nothing outside Netskope's interception was trusted either.

Fix

  • Fall back to Cert:\LocalMachine\Root (CN certadmin) and Cert:\LocalMachine\CA (CN ca.<tenant>.goskope.com) when the PEM files are absent or encrypted, mirroring the macOS keychain fallback, and always add the intermediate from the store when a source yields the root alone. The exact PowerShell was verified on the affected machine through its own Python.
  • Replace the fingerprint check with pure-Python base64 body matching (as fumitm.py already does) and append only the blocks a bundle lacks.
  • Never seed a tool bundle from a file that holds only the proxy certificate, rebuild our own bundle from the Windows store when it does, and warn instead of silently creating an empty bundle when the system export returns nothing.

Tests

13 new cases in test_suite/test_fumitm_windows.py covering store export parsing, the encrypted-file fallback, root-only sources gaining the intermediate, every-block existence checks, idempotent appends, and provider-only bundle repair. All 25 cases in the file pass (run on macOS with winreg stubbed; CI runs them on windows-latest). ruff and the code-quality suite pass.

Not in this PR

setup_gcloud_cert on Windows assumes gcloud reads the Windows certificate store. It does not; it falls back to REQUESTS_CA_BUNDLE when core/custom_ca_certs_file is unset, which is why the Python bundle fix is enough here. That assumption deserves its own change.

joshblease and others added 3 commits October 26, 2026 17:14
…keep the intermediate

The hardened Netskope client leaves only nscacert.pem.enc on disk, so
fumitm_windows.py told the user to export the root by hand. A hand-exported
root alone then failed the Google Cloud SDK installer with:

  CA cert does not include key usage extension

The Netskope root carries no Key Usage extension, and Python 3.13+ turns on
VERIFY_X509_STRICT by default, which rejects any chain that reaches it. With
the tenant intermediate (which has a Key Usage extension) in the bundle,
VERIFY_X509_PARTIAL_CHAIN stops chain building there and the root is never
examined. A root-only bundle reproduces the failure on 3.13 and 3.14 and
passes on 3.12; root plus intermediate passes on all three.

- Fall back to Cert:\LocalMachine\Root (CN certadmin) and
  Cert:\LocalMachine\CA (CN ca.<tenant>.goskope.com) when the PEM files are
  absent or encrypted, mirroring the macOS keychain fallback, and always add
  the intermediate from the store when a source yields the root alone.
- Replace the install-mode fingerprint check, which unlinked an open
  temporary file (refused on Windows) and so appended another copy of the
  root on every run, with pure-Python body matching. Append only the blocks
  a bundle lacks.
- Never seed a tool bundle from a file that holds only the proxy
  certificate, and rebuild our own bundle from the Windows store when it
  does; warn instead of silently creating an empty bundle when the system
  export returns nothing.
Address the Codex and agy reviews of this pull request.

- Assemble every rebuilt bundle in a temporary file and move it into
  place with os.replace. Write nothing when the Windows root export is
  empty, and set environment variables only after a write succeeds.
- Top up our own bundle in place when it lacks a provider certificate,
  such as a newly added intermediate, instead of rebuilding it from the
  store and risking its system roots.
- Treat an unreadable bundle, or a certificate file with no certificate,
  as unknown rather than "provider-only" or "already present".
- Parse only complete BEGIN/END pairs, and append each missing
  certificate once even when the source repeats it.
- Query the certificate store with exact common names, only currently
  valid certificates, and errors that stop the script. Return every
  intermediate issued by the store's root, and always ask the store
  rather than inferring the intermediate from a certificate count.
- Export system roots from both the LocalMachine and CurrentUser Root
  stores.
- Install each certificate of the chain separately into the Windows
  stores and the Java keystore (alias, alias-2), since X509Certificate2
  and keytool read only the first certificate in a file.
- Report provider-only bundles in status for Python, npm, gcloud and Git,
  and replace them during --fix. NODE_EXTRA_CA_CERTS is exempt because
  it adds to Node's built-in roots.
- Detect the stAgentSvc.exe service and encrypted .enc certificate files
  in the Netskope connection check, and remove the status temporary file
  even when a check fails.
- Run the Windows tests on macOS and Linux with a winreg stub, move the
  __main__ block to the end, and add tests for each case above.
Address the agy follow-up review.

- check_java_status requires an alias for each certificate of the chain
  (alias, alias-2), as setup_java_cert imports them.
- setup_git_cert no longer creates the directory of a missing
  http.sslCAInfo file before the system export is known to be non-empty,
  and falls back to the managed bundle when the build fails.
- Remove a duplicated docstring line and show the per-certificate file in
  the Java dry-run message.
@aberoham
aberoham merged commit e4b6dff into aberoham:main Oct 26, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants