diff --git a/README.md b/README.md index 3ea8c66..4335a06 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Certificate installation scripts -Scripts to install a CA certificate, configure Node/npm and Python (pip, uv, Hugging Face Hub, and related TLS clients), and clear Docker Hub credentials that can break redirected Docker Hub pulls. +Scripts to install a CA certificate, configure Node/npm, Python (pip, uv, Hugging Face Hub, and related TLS clients), Ruby where supported, and clear Docker Hub credentials that can break redirected Docker Hub pulls. This document describes the certificate installation and validation scripts for **macOS**, **Linux (Debian/Ubuntu)**, and **Windows**. @@ -10,7 +10,7 @@ This document describes the certificate installation and validation scripts for | **validate_install_macos.sh** | macOS | Validate PEM and env config | | **install_certs_debian_ubuntu.sh** | Debian/Ubuntu | Install cert into system trust + profile.d + user shell rc + Docker cleanup | | **validate_certs_debian_ubuntu.sh** | Debian/Ubuntu | Validate PEM and env config | -| **install_certs_windows.ps1** | Windows | Install cert, set env vars (Node/Python), and clear Docker Hub credentials | +| **install_certs_windows.ps1** | Windows | Install cert, set env vars (Node/Python/Ruby), and clear Docker Hub credentials | | **validate_install_windows.ps1** | Windows | Validate PEM and env config | Environment variables by platform (see each section for details): @@ -22,7 +22,7 @@ Environment variables by platform (see each section for details): | `UV_NATIVE_TLS=true` / `1` | Python **uv** | macOS uses `true`; Windows uses `1`; **not** set by the Debian/Ubuntu script | | `UV_SYSTEM_CERTS=true` | Python **uv** | Set by macOS for **`python`**, **`huggingface`**, or **`all`** | | `REQUESTS_CA_BUNDLE=` | Python **requests** / many HTTPS stacks | PEM or bundle path | -| `SSL_CERT_FILE=` | OpenSSL-backed tools | Set on **Debian/Ubuntu** for **`python`**, **`huggingface`**, or **`all`** to the system CA bundle | +| `SSL_CERT_FILE=` | OpenSSL-backed tools, including Ruby | Set on **Debian/Ubuntu** for **`python`**, **`huggingface`**, or **`all`** to the system CA bundle; set on **Windows** for **`python`**, **`huggingface`**, **`ruby`**, or **`all`** to a generated bundle | | `HF_HUB_DISABLE_XET=1` | Python **huggingface_hub** | Set when **`huggingface` or `all`**: disables XET (not supported with typical MITM / Artifactory redirect flows) | | `HF_HUB_ETAG_TIMEOUT=86400` | Python **huggingface_hub** | Set when **`huggingface` or `all`**: ETag check timeout (seconds); reduces spurious failures on slow paths | | `HF_HUB_DOWNLOAD_TIMEOUT=86400` | Python **huggingface_hub** | Set when **`huggingface` or `all`**: download timeout (seconds) | @@ -203,7 +203,7 @@ Tests are black-box (exit codes and stderr). #### Windows tests -**test_install_certs_windows.ps1** runs automated tests for **install_certs_windows.ps1** (CLI and parameter validation; **-UseCert -Package python** sets Python TLS only and leaves existing `HF_HUB_*` unchanged; **-Package huggingface** adds `HF_HUB_*`; **-Package all** sets npm + TLS + HF; when run as admin) and **validate_install_windows.ps1** (-ExpectedSubject required, env-based validation: valid PEM, missing file, invalid PEM, subject match and no-match, and system-level env when run as admin). **Run the test script as Administrator** so install script tests and system-level validate tests execute; the script uses a temp directory and an embedded PEM. +**test_install_certs_windows.ps1** runs automated tests for **install_certs_windows.ps1** (CLI and parameter validation; **-UseCert -Package python** sets Python TLS to a generated bundle and leaves existing `HF_HUB_*` unchanged; **-Package ruby** sets `SSL_CERT_FILE`; **-Package huggingface** adds `HF_HUB_*`; **-Package all** sets npm + TLS + Ruby + HF; when run as admin) and **validate_install_windows.ps1** (-ExpectedSubject required, env-based validation: valid PEM, missing file, invalid PEM, subject match and no-match, and system-level env when run as admin). **Run the test script as Administrator** so install script tests and system-level validate tests execute; the script uses a temp directory and an embedded PEM. **Requirements:** Windows with PowerShell. The install and validate scripts must be in the parent of `testing/` (repo root). @@ -218,8 +218,8 @@ Exit code 0 if all tests pass, 1 otherwise. Output shows pass/fail per test and | Area | Covered | |------|--------| -| **install_certs_windows.ps1** | When run as admin: script passes admin check (no "must run as Administrator" error). No cert source (parameter set error); invalid `-Package`; `-CertName` without `-ExtractPath` (and reverse); `-UseCert` and `-CertName` together; `-UseCert` with nonexistent file; `-UseCert` with invalid PEM; `-UseCert` with valid PEM (no "not a file" or "Invalid PEM" error). **Packages:** `-UseCert -Package python` sets TLS-only Machine vars and does not remove pre-existing `HF_HUB_*`; `-Package huggingface` adds `HF_HUB_*`; `-Package all` sets npm + TLS + HF. | -| **validate_install_windows.ps1** | `-ExpectedSubject` required (exit 1 if missing); current user env (no paths → exit 0); env path to valid PEM (exit 0), missing file (exit 1), invalid PEM (exit 1); subject mismatch (exit 1, FAIL message); system-level (Machine) env when run as admin. | +| **install_certs_windows.ps1** | When run as admin: script passes admin check (no "must run as Administrator" error). No cert source (parameter set error); invalid `-Package`; `-CertName` without `-ExtractPath` (and reverse); `-UseCert` and `-CertName` together; `-UseCert` with nonexistent file; `-UseCert` with invalid PEM; `-UseCert` with valid PEM (no "not a file" or "Invalid PEM" error). **Packages:** `-UseCert -Package python` sets TLS-only Machine vars to a generated bundle and does not remove pre-existing `HF_HUB_*`; `-Package ruby` sets `SSL_CERT_FILE` to the generated bundle; `-Package huggingface` adds `HF_HUB_*`; `-Package all` sets npm + TLS + Ruby + HF. | +| **validate_install_windows.ps1** | `-ExpectedSubject` required (exit 1 if missing); current user env (no paths → exit 0); env path to valid PEM (exit 0), missing file (exit 1), invalid PEM (exit 1); subject mismatch (exit 1, FAIL message); system-level (Machine) env when run as admin. Reads `NODE_EXTRA_CA_CERTS`, `REQUESTS_CA_BUNDLE`, and `SSL_CERT_FILE`. | Tests are black-box (exit codes and stdout/stderr). Paths are passed to the validate script via a temp file when invoking as a child process to avoid command-line parsing issues with backslashes. @@ -353,14 +353,14 @@ sudo ./validate_certs_debian_ubuntu.sh --all-users --expected-subject "O=Example ### Overview -`install_certs_windows.ps1` configures **Node/npm** and/or **Python** on Windows to use a custom CA certificate. **It must be run as Administrator (or SYSTEM);** the script exits with an error otherwise. +`install_certs_windows.ps1` configures **Node/npm**, **Python**, and/or **Ruby** on Windows to use a custom CA certificate. **It must be run as Administrator (or SYSTEM);** the script exits with an error otherwise. - Either **extracts** a certificate from the Windows cert store (LocalMachine\Root) by **subject substring** (`-CertName`), or **uses an existing** PEM file you provide (**-UseCert**). If **multiple** certs match the pattern, the script logs a warning and picks one (prefers a subject containing `Root`, otherwise the first match). -- With **-CertName** and **-ExtractPath:** writes **package-route.pem** per user under each user’s profile and sets **User**-level env vars in the registry for each user. **npm:** `NODE_USE_SYSTEM_CA`, `NODE_EXTRA_CA_CERTS`. **Python TLS** (`python`, `huggingface`, or `all`): `UV_NATIVE_TLS`, `REQUESTS_CA_BUNDLE`. **Hugging Face Hub** (`huggingface` or `all`): `HF_HUB_DISABLE_XET`, `HF_HUB_ETAG_TIMEOUT`, `HF_HUB_DOWNLOAD_TIMEOUT`. -- With **-UseCert:** does **not** write a PEM file; sets **Machine**-level env vars. The script **deletes** overlapping **User**-level vars so they do not override Machine (User wins over Machine on Windows). Which vars are set or cleared depends on `-Package` (see env table above). +- With **-CertName** and **-ExtractPath:** writes **package-route.pem** per user under each user’s profile and sets **User**-level env vars in the registry for each user. When Python or Ruby is configured, this file is a combined bundle: the selected cert first, then Windows trusted roots, then valid certs from previously configured bundles (dedupe by SHA-256 fingerprint). **npm:** `NODE_USE_SYSTEM_CA`, `NODE_EXTRA_CA_CERTS`. **Python TLS** (`python`, `huggingface`, or `all`): `UV_NATIVE_TLS`, `REQUESTS_CA_BUNDLE`, `SSL_CERT_FILE`. **Ruby** (`ruby` or `all`): `SSL_CERT_FILE`. **Hugging Face Hub** (`huggingface` or `all`): `HF_HUB_DISABLE_XET`, `HF_HUB_ETAG_TIMEOUT`, `HF_HUB_DOWNLOAD_TIMEOUT`. +- With **-UseCert:** keeps npm pointed at your PEM, and writes a generated combined bundle under `C:\ProgramData\package-reroute\package-route-bundle.pem` for Python/Ruby OpenSSL-style clients. The script sets **Machine**-level env vars and **deletes** overlapping **User**-level vars so they do not override Machine (User wins over Machine on Windows). Which vars are set or cleared depends on `-Package` (see env table above). - Clears Docker Hub credentials for the current PowerShell user. When running as `SYSTEM`, this cleanup is skipped with a warning because it must run in the user's Windows session. -Re-runs **merge** certs: if the target file already exists, the script saves its content, overwrites with the new cert, then appends other certs from the saved copy (dedupe by SHA-256 fingerprint). So running with a second cert adds it to the bundle instead of replacing it. +Re-runs **merge** certs: generated bundles put the custom cert first, append Windows roots, then append valid certs from previous bundles (dedupe by SHA-256 fingerprint). So running with a second cert can preserve existing custom certs instead of replacing them. ### Requirements @@ -381,7 +381,7 @@ powershell -ExecutionPolicy Bypass -File install_certs_windows.ps1 -Package all | Parameter | Required | Description | |-----------|----------|-------------| -| `-Package` | No (default: **all**) | `npm`, `python`, `huggingface`, or `all` (all = npm + Python TLS + Hugging Face Hub). | +| `-Package` | No (default: **all**) | `npm`, `python`, `huggingface`, `ruby`, or `all` (all = npm + Python TLS + Ruby + Hugging Face Hub). | | `-CertName` | Yes* | Substring used to match cert **Subject** in the store (`*CertName*` wildcard). Requires `-ExtractPath`. Cannot be used with `-UseCert`. | | `-ExtractPath` | Yes* | Directory under each user’s profile for **package-route.pem** (rooted paths are normalized to a folder under the profile, same idea as macOS). Requires `-CertName`. | | `-UseCert` | Yes* | Path to an existing PEM file. Cannot be used with `-CertName` / `-ExtractPath`. | @@ -396,7 +396,7 @@ powershell -ExecutionPolicy Bypass -File install_certs_windows.ps1 -Package all .\install_certs_windows.ps1 -Package all -CertName "Your Org Root CA" -ExtractPath certs\npm ``` -**Use an existing PEM (Machine-level env; User-level cert vars are deleted):** +**Use an existing PEM (Machine-level env; Python/Ruby use a generated combined bundle; User-level cert vars are deleted):** ```powershell .\install_certs_windows.ps1 -Package all -UseCert C:\Users\Administrator\other-ca\company-ca.pem @@ -412,8 +412,8 @@ powershell -ExecutionPolicy Bypass -File install_certs_windows.ps1 -Package all - **Admin required.** The script must be run as Administrator (or SYSTEM). - **Cert source:** either store (`-CertName` + `-ExtractPath`) or file (`-UseCert`). -- **Extract path:** per-user **package-route.pem** and User-level env per `-Package` (npm / Python TLS / Hugging Face as above); re-runs merge and dedupe by fingerprint. Machine-level cert vars are **cleared** so only User applies (avoids duplication if you previously used -UseCert). -- **UseCert:** no PEM written; Machine-level env set per `-Package`; **`python`** does not set `HF_HUB_*` and does **not** clear pre-existing `HF_HUB_*` on the target scope. User-level cert vars are **deleted** so only Machine applies when using `-UseCert` as admin. +- **Extract path:** per-user **package-route.pem** and User-level env per `-Package` (npm / Python TLS / Ruby / Hugging Face as above); when Python or Ruby is selected, the file is a combined bundle of the custom cert plus Windows roots. Machine-level cert vars are **cleared** so only User applies (avoids duplication if you previously used -UseCert). +- **UseCert:** npm points at the supplied PEM; Python/Ruby point at the generated combined bundle under `C:\ProgramData\package-reroute\package-route-bundle.pem`; Machine-level env is set per `-Package`; **`python`** does not set `HF_HUB_*` and does **not** clear pre-existing `HF_HUB_*` on the target scope. User-level cert vars are **deleted** so only Machine applies when using `-UseCert` as admin. - **Docker:** clears Docker Hub credentials for the current PowerShell user. Run the script in the user's session for this step; `SYSTEM` cannot clean up the user's Docker credential store. Users must start a **new terminal** for env changes to take effect. @@ -427,7 +427,7 @@ Users must start a **new terminal** for env changes to take effect. | Parameter | Description | |-----------|-------------| | `-ExpectedSubject ` | **Required.** At least one cert in each PEM file (bundle) must have a subject matching `` (case-insensitive). | -| *(default scope)* | Read `NODE_EXTRA_CA_CERTS` and `REQUESTS_CA_BUNDLE` from the current user's environment (User then Machine), then validate each referenced PEM file. | +| *(default scope)* | Read `NODE_EXTRA_CA_CERTS`, `REQUESTS_CA_BUNDLE`, and `SSL_CERT_FILE` from the current user's environment (User then Machine), then validate each referenced PEM file. | | `-AllUsers` | **(Admin only.)** For each user in `C:\Users\*`, read their User registry env, resolve cert paths, and validate each PEM. | **Exit code:** 0 if all checks passed, 1 if any check failed. diff --git a/install_certs_windows.ps1 b/install_certs_windows.ps1 index 12a7ec9..7f6bc21 100644 --- a/install_certs_windows.ps1 +++ b/install_certs_windows.ps1 @@ -1,26 +1,26 @@ # (c) JFrog Ltd. (2026) -# Auto-Extract certificate from Windows store (or use existing PEM) and configure Node/npm and/or Python for Windows +# Auto-Extract certificate from Windows store (or use existing PEM) and configure Node/npm, Python, and/or Ruby for Windows # Run: powershell -ExecutionPolicy Bypass -File install_certs_windows.ps1 -Package all -CertName "Your Org Root CA" -ExtractPath certs\npm # Or: powershell -ExecutionPolicy Bypass -File install_certs_windows.ps1 -Package all -UseCert C:\path\to\ca.pem # # Parameters: -# -Package npm|python|huggingface|all -# npm / python / huggingface / all (all = npm + python TLS + Hugging Face Hub) +# -Package npm|python|huggingface|ruby|all +# npm / python / huggingface / ruby / all (all = npm + Python TLS + Ruby + Hugging Face Hub) # -CertName Substring to match cert subject (errors if 0 or >1 match). Requires -ExtractPath. Cannot be used with -UseCert. # -ExtractPath Directory for the PEM (writes \package-route.pem); relative to each user's profile or absolute. Requires -CertName. # -UseCert Path to an existing PEM cert file. Cannot be used with -CertName/-ExtractPath. # # Either (-CertName AND -ExtractPath) OR -UseCert must be provided. -# If user had a different env path, it is replaced with the new path; new PEM is first, other PEMs from the old file are appended (dedupe by fingerprint). +# If user had a different env path, it is replaced with the new path; custom PEM is first, Windows roots and other PEMs from old files are appended (dedupe by fingerprint). # # Must run as Administrator (or SYSTEM). Exits with error otherwise. # When run as SYSTEM/admin with -CertName: installs PEM and User-level env per user (each user's profile). -# When run with -UseCert: sets Machine-level env to that path; no per-user PEM. +# When run with -UseCert: sets Machine-level env; Python/Ruby use a generated Machine-level bundle under ProgramData. # Also performs best-effort Docker Hub credential cleanup for the current user. param( [Parameter(Mandatory = $false)] - [ValidateSet("npm", "python", "huggingface", "all")] + [ValidateSet("npm", "python", "huggingface", "ruby", "all")] [string]$Package = "all", [Parameter(ParameterSetName = "Extract", Mandatory = $true)] @@ -48,6 +48,8 @@ $utf8NoBom = [System.Text.UTF8Encoding]::new($false) function DoNpm { $Package -eq 'npm' -or $Package -eq 'all' } function DoPythonTls { $Package -eq 'python' -or $Package -eq 'huggingface' -or $Package -eq 'all' } function DoHuggingface { $Package -eq 'huggingface' -or $Package -eq 'all' } +function DoRuby { $Package -eq 'ruby' -or $Package -eq 'all' } +function DoOpenSslBundle { (DoPythonTls) -or (DoRuby) } $DockerHubKeys = @( "https://index.docker.io/v1/", @@ -256,6 +258,88 @@ function Write-MergedPemFile { if ($savedTarget -and (Test-Path -LiteralPath $savedTarget -PathType Leaf)) { Remove-Item -LiteralPath $savedTarget -Force -ErrorAction SilentlyContinue } } +function Convert-CertToPemBlock { + param([System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert) + if (-not $Cert) { return $null } + return "-----BEGIN CERTIFICATE-----`n" + [System.Convert]::ToBase64String($Cert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END CERTIFICATE-----" +} + +function Add-PemBlockToBundleList { + param([string]$PemBlock, [System.Collections.ArrayList]$Blocks, [hashtable]$SeenFingerprints) + if ([string]::IsNullOrWhiteSpace($PemBlock)) { return 0 } + $fp = Get-PemFingerprint -PemBlock $PemBlock + if (-not $fp) { return 0 } + if ($SeenFingerprints.ContainsKey($fp)) { return 0 } + $SeenFingerprints[$fp] = $true + [void]$Blocks.Add($PemBlock) + return 1 +} + +function Get-WindowsRootPemBlocks { + $blocks = New-Object System.Collections.ArrayList + foreach ($storePath in @("Cert:\LocalMachine\Root", "Cert:\CurrentUser\Root")) { + if (-not (Test-Path $storePath)) { continue } + foreach ($cert in @(Get-ChildItem $storePath -ErrorAction SilentlyContinue)) { + $pem = Convert-CertToPemBlock -Cert $cert + if ($pem) { [void]$blocks.Add($pem) } + } + } + return $blocks.ToArray() +} + +function Write-CombinedPemBundle { + param([string]$TargetPath, [string[]]$CustomPemBlocks, [string[]]$OldPaths) + $TargetPath = [System.IO.Path]::GetFullPath($TargetPath) + $targetDir = Split-Path -Parent $TargetPath + if (-not (Test-Path -LiteralPath $targetDir -PathType Container)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $savedTarget = $null + if (Test-Path -LiteralPath $TargetPath -PathType Leaf) { + $content = [System.IO.File]::ReadAllText($TargetPath, $utf8NoBom) + if ($content.Length -gt 0) { + $savedTarget = [System.IO.Path]::GetTempFileName() + [System.IO.File]::WriteAllText($savedTarget, $content, $utf8NoBom) + } + } + + $bundleBlocks = New-Object System.Collections.ArrayList + $seen = @{} + foreach ($block in $CustomPemBlocks) { + Add-PemBlockToBundleList -PemBlock $block -Blocks $bundleBlocks -SeenFingerprints $seen | Out-Null + } + + $windowsRootCount = 0 + foreach ($block in Get-WindowsRootPemBlocks) { + $windowsRootCount += Add-PemBlockToBundleList -PemBlock $block -Blocks $bundleBlocks -SeenFingerprints $seen + } + + if ($savedTarget -and (Test-Path -LiteralPath $savedTarget -PathType Leaf)) { + foreach ($block in Get-PemBlocksFromFile -Path $savedTarget) { + Add-PemBlockToBundleList -PemBlock $block -Blocks $bundleBlocks -SeenFingerprints $seen | Out-Null + } + } + + foreach ($p in $OldPaths) { + if ([string]::IsNullOrWhiteSpace($p)) { continue } + if (Test-SamePath -Path1 $p -Path2 $TargetPath) { continue } + if (-not (Test-Path -LiteralPath $p -PathType Leaf)) { continue } + foreach ($block in Get-PemBlocksFromFile -Path $p) { + Add-PemBlockToBundleList -PemBlock $block -Blocks $bundleBlocks -SeenFingerprints $seen | Out-Null + } + } + + if ($bundleBlocks.Count -eq 0) { + if ($savedTarget -and (Test-Path -LiteralPath $savedTarget -PathType Leaf)) { Remove-Item -LiteralPath $savedTarget -Force -ErrorAction SilentlyContinue } + Write-Host "[Error] Could not build a PEM bundle: no valid certificates found." -ForegroundColor Red + exit 1 + } + + [System.IO.File]::WriteAllText($TargetPath, (($bundleBlocks.ToArray()) -join "`n"), $utf8NoBom) + if ($savedTarget -and (Test-Path -LiteralPath $savedTarget -PathType Leaf)) { Remove-Item -LiteralPath $savedTarget -Force -ErrorAction SilentlyContinue } + Write-Host " + Wrote combined PEM bundle ($($bundleBlocks.Count) certs, $windowsRootCount from Windows roots): $TargetPath" +} + # Get current env var value for the given scope (Machine or User). function Get-EnvPath { param([string]$VarName, [string]$Scope) @@ -265,15 +349,17 @@ function Get-EnvPath { return $val } -# Get User env paths (NODE_EXTRA_CA_CERTS, REQUESTS_CA_BUNDLE) for a user by loading their registry hive. Returns array of existing file paths. +# Get User env paths (NODE_EXTRA_CA_CERTS, REQUESTS_CA_BUNDLE, SSL_CERT_FILE) for a user by loading their registry hive. Returns array of existing file paths. function Get-UserEnvCertPaths { param([string]$ProfilePath, [string]$Scope) if ($Scope -eq "Machine") { return @() } $paths = @() $nodePath = Get-EnvPath -VarName "NODE_EXTRA_CA_CERTS" -Scope "User" $pipPath = Get-EnvPath -VarName "REQUESTS_CA_BUNDLE" -Scope "User" + $sslPath = Get-EnvPath -VarName "SSL_CERT_FILE" -Scope "User" if ($nodePath -and (Test-Path -LiteralPath $nodePath -PathType Leaf) -and $paths -notcontains $nodePath) { $paths += $nodePath } if ($pipPath -and (Test-Path -LiteralPath $pipPath -PathType Leaf) -and $paths -notcontains $pipPath) { $paths += $pipPath } + if ($sslPath -and (Test-Path -LiteralPath $sslPath -PathType Leaf) -and $paths -notcontains $sslPath) { $paths += $sslPath } return $paths } @@ -310,8 +396,10 @@ function Get-OtherUserEnvCertPaths { if (Test-Path -LiteralPath $keyPath) { $nodePath = (Get-ItemProperty -Path $keyPath -Name "NODE_EXTRA_CA_CERTS" -ErrorAction SilentlyContinue).NODE_EXTRA_CA_CERTS $pipPath = (Get-ItemProperty -Path $keyPath -Name "REQUESTS_CA_BUNDLE" -ErrorAction SilentlyContinue).REQUESTS_CA_BUNDLE + $sslPath = (Get-ItemProperty -Path $keyPath -Name "SSL_CERT_FILE" -ErrorAction SilentlyContinue).SSL_CERT_FILE if ($nodePath -and (Test-Path -LiteralPath $nodePath -PathType Leaf) -and $paths -notcontains $nodePath) { $paths += $nodePath } if ($pipPath -and (Test-Path -LiteralPath $pipPath -PathType Leaf) -and $paths -notcontains $pipPath) { $paths += $pipPath } + if ($sslPath -and (Test-Path -LiteralPath $sslPath -PathType Leaf) -and $paths -notcontains $sslPath) { $paths += $sslPath } } } finally { if ($weLoaded -and $tempKey) { & reg.exe unload "HKU\$tempKey" 2>&1 | Out-Null } @@ -321,7 +409,7 @@ function Get-OtherUserEnvCertPaths { # Set User env vars for another user. If hive already loaded use HKU\; else load NTUSER.DAT. function Set-OtherUserEnvVars { - param([string]$ProfilePath, [string]$CertPath, [bool]$DoNpm, [bool]$DoPythonTls, [bool]$DoHuggingface) + param([string]$ProfilePath, [string]$CertPath, [string]$BundlePath, [bool]$DoNpm, [bool]$DoPythonTls, [bool]$DoHuggingface, [bool]$DoRuby) $sid = Get-UserSidFromProfile -ProfilePath $ProfilePath $keyPath = $null $weLoaded = $false @@ -345,7 +433,11 @@ function Set-OtherUserEnvVars { } if ($DoPythonTls) { Set-ItemProperty -Path $keyPath -Name "UV_NATIVE_TLS" -Value "1" -Type String -Force - Set-ItemProperty -Path $keyPath -Name "REQUESTS_CA_BUNDLE" -Value $CertPath -Type String -Force + Set-ItemProperty -Path $keyPath -Name "REQUESTS_CA_BUNDLE" -Value $BundlePath -Type String -Force + Set-ItemProperty -Path $keyPath -Name "SSL_CERT_FILE" -Value $BundlePath -Type String -Force + } + if ($DoRuby) { + Set-ItemProperty -Path $keyPath -Name "SSL_CERT_FILE" -Value $BundlePath -Type String -Force } if ($DoHuggingface) { Set-ItemProperty -Path $keyPath -Name "HF_HUB_DISABLE_XET" -Value "1" -Type String -Force @@ -380,6 +472,8 @@ if ($PSCmdlet.ParameterSetName -eq "UseCert") { } $certStore = if ($isSystemContext -or $isAdmin) { "LocalMachine" } else { "CurrentUser" } +$programDataRoot = if ($env:PACKAGE_REROUTE_PROGRAMDATA) { $env:PACKAGE_REROUTE_PROGRAMDATA } elseif ($env:ProgramData) { $env:ProgramData } else { "C:\ProgramData" } +$machineBundlePath = Join-Path (Join-Path $programDataRoot "package-reroute") "package-route-bundle.pem" # --- Extract from store (when -CertName -ExtractPath): get PEM once --- @@ -408,12 +502,15 @@ if ($PSCmdlet.ParameterSetName -eq "Extract") { # --- Per-user install when Extract: PEM and User env per user --- -# Under each user's profile: strip leading slashes; if path is absolute, use path relative to drive so we still get per-user dir (e.g. C:\certs -> certs). -$extractPathTrim = $ExtractPath.TrimStart('\', '/') -if ([System.IO.Path]::IsPathRooted($extractPathTrim)) { - $extractPathTrim = $extractPathTrim.TrimStart([System.IO.Path]::GetPathRoot($extractPathTrim)).TrimStart('\', '/') +$extractPathTrim = "certs" +if ($PSCmdlet.ParameterSetName -eq "Extract") { + # Under each user's profile: strip leading slashes; if path is absolute, use path relative to drive so we still get per-user dir (e.g. C:\certs -> certs). + $extractPathTrim = $ExtractPath.TrimStart('\', '/') + if ([System.IO.Path]::IsPathRooted($extractPathTrim)) { + $extractPathTrim = $extractPathTrim.TrimStart([System.IO.Path]::GetPathRoot($extractPathTrim)).TrimStart('\', '/') + } + if ([string]::IsNullOrWhiteSpace($extractPathTrim)) { $extractPathTrim = "certs" } } -if ([string]::IsNullOrWhiteSpace($extractPathTrim)) { $extractPathTrim = "certs" } if ($PSCmdlet.ParameterSetName -eq "Extract" -and $null -ne $extractedPem) { if ($isSystemContext -or $isAdmin) { @@ -425,12 +522,16 @@ if ($PSCmdlet.ParameterSetName -eq "Extract" -and $null -ne $extractedPem) { $certPath = Join-Path $certDir "package-route.pem" $oldPaths = Get-OtherUserEnvCertPaths -ProfilePath $userHome if (-not (Test-Path $certDir)) { New-Item -ItemType Directory -Path $certDir -Force | Out-Null } - Write-MergedPemFile -TargetPath $certPath -NewPem $extractedPem -OldPaths $oldPaths + if (DoOpenSslBundle) { + Write-CombinedPemBundle -TargetPath $certPath -CustomPemBlocks @($extractedPem) -OldPaths $oldPaths + } else { + Write-MergedPemFile -TargetPath $certPath -NewPem $extractedPem -OldPaths $oldPaths + } if (-not (Test-ValidPemFile -Path $certPath)) { Write-Host "[Error] Extracted PEM file is invalid: $certPath" -ForegroundColor Red exit 1 } - Set-OtherUserEnvVars -ProfilePath $userHome -CertPath $certPath -DoNpm:(DoNpm) -DoPythonTls:(DoPythonTls) -DoHuggingface:(DoHuggingface) + Set-OtherUserEnvVars -ProfilePath $userHome -CertPath $certPath -BundlePath $certPath -DoNpm:(DoNpm) -DoPythonTls:(DoPythonTls) -DoHuggingface:(DoHuggingface) -DoRuby:(DoRuby) Write-Host " + $userHome : $certPath" } } else { @@ -439,7 +540,11 @@ if ($PSCmdlet.ParameterSetName -eq "Extract" -and $null -ne $extractedPem) { $certPath = Join-Path $certDir "package-route.pem" $oldPaths = Get-UserEnvCertPaths -ProfilePath $env:USERPROFILE -Scope "User" if (-not (Test-Path $certDir)) { New-Item -ItemType Directory -Path $certDir -Force | Out-Null } - Write-MergedPemFile -TargetPath $certPath -NewPem $extractedPem -OldPaths $oldPaths + if (DoOpenSslBundle) { + Write-CombinedPemBundle -TargetPath $certPath -CustomPemBlocks @($extractedPem) -OldPaths $oldPaths + } else { + Write-MergedPemFile -TargetPath $certPath -NewPem $extractedPem -OldPaths $oldPaths + } if (-not (Test-ValidPemFile -Path $certPath)) { Write-Host "[Error] Extracted PEM file is invalid: $certPath" -ForegroundColor Red exit 1 @@ -451,16 +556,21 @@ if ($PSCmdlet.ParameterSetName -eq "Extract" -and $null -ne $extractedPem) { if (DoPythonTls) { [Environment]::SetEnvironmentVariable("UV_NATIVE_TLS", "1", "User") [Environment]::SetEnvironmentVariable("REQUESTS_CA_BUNDLE", $certPath, "User") + [Environment]::SetEnvironmentVariable("SSL_CERT_FILE", $certPath, "User") + } + if (DoRuby) { + [Environment]::SetEnvironmentVariable("SSL_CERT_FILE", $certPath, "User") } if (DoHuggingface) { [Environment]::SetEnvironmentVariable("HF_HUB_DISABLE_XET", "1", "User") [Environment]::SetEnvironmentVariable("HF_HUB_ETAG_TIMEOUT", "86400", "User") [Environment]::SetEnvironmentVariable("HF_HUB_DOWNLOAD_TIMEOUT", "86400", "User") } - Write-Host " + NODE_USE_SYSTEM_CA and NODE_EXTRA_CA_CERTS set." + if (DoNpm) { Write-Host " + NODE_USE_SYSTEM_CA and NODE_EXTRA_CA_CERTS set." } if (DoPythonTls) { - Write-Host " + UV_NATIVE_TLS set; REQUESTS_CA_BUNDLE set to $certPath" + Write-Host " + UV_NATIVE_TLS set; REQUESTS_CA_BUNDLE and SSL_CERT_FILE set to $certPath" } + if ((DoRuby) -and -not (DoPythonTls)) { Write-Host " + SSL_CERT_FILE set to $certPath" } if (DoHuggingface) { Write-Host " + Hugging Face Hub timeouts / HF_HUB_DISABLE_XET set." } } # Only clear the other scope after new User vars are set (above); on failure we exit 1 before reaching here. @@ -472,6 +582,9 @@ if ($PSCmdlet.ParameterSetName -eq "Extract" -and $null -ne $extractedPem) { if (DoPythonTls) { [Environment]::SetEnvironmentVariable("UV_NATIVE_TLS", $null, "Machine") [Environment]::SetEnvironmentVariable("REQUESTS_CA_BUNDLE", $null, "Machine") + [Environment]::SetEnvironmentVariable("SSL_CERT_FILE", $null, "Machine") + } elseif (DoRuby) { + [Environment]::SetEnvironmentVariable("SSL_CERT_FILE", $null, "Machine") } Write-Host " + Cleared Machine-level cert vars so only User settings apply." } @@ -480,6 +593,24 @@ if ($PSCmdlet.ParameterSetName -eq "Extract" -and $null -ne $extractedPem) { if ($PSCmdlet.ParameterSetName -eq "UseCert") { $envScope = if ($isSystemContext -or $isAdmin) { "Machine" } else { "User" } + $bundlePath = $UseCert + if (DoOpenSslBundle) { + $oldBundlePaths = @() + foreach ($scope in @("Machine", "User")) { + foreach ($var in @("REQUESTS_CA_BUNDLE", "SSL_CERT_FILE")) { + $oldPath = Get-EnvPath -VarName $var -Scope $scope + if ($oldPath -and (Test-Path -LiteralPath $oldPath -PathType Leaf) -and $oldBundlePaths -notcontains $oldPath) { + $oldBundlePaths += $oldPath + } + } + } + $bundlePath = if ($envScope -eq "Machine") { $machineBundlePath } else { Join-Path (Join-Path $env:USERPROFILE "certs") "package-route-bundle.pem" } + Write-CombinedPemBundle -TargetPath $bundlePath -CustomPemBlocks @(Get-PemBlocksFromFile -Path $UseCert) -OldPaths $oldBundlePaths + if (-not (Test-ValidPemFile -Path $bundlePath)) { + Write-Host "[Error] Generated PEM bundle is invalid: $bundlePath" -ForegroundColor Red + exit 1 + } + } Write-Host "[2/4] Setting Environment Variables ($envScope)..." if (DoNpm) { [Environment]::SetEnvironmentVariable("NODE_USE_SYSTEM_CA", "1", $envScope) @@ -488,8 +619,9 @@ if ($PSCmdlet.ParameterSetName -eq "UseCert") { } if (DoPythonTls) { [Environment]::SetEnvironmentVariable("UV_NATIVE_TLS", "1", $envScope) - [Environment]::SetEnvironmentVariable("REQUESTS_CA_BUNDLE", $UseCert, $envScope) - Write-Host " + UV_NATIVE_TLS set; REQUESTS_CA_BUNDLE set to $UseCert" + [Environment]::SetEnvironmentVariable("REQUESTS_CA_BUNDLE", $bundlePath, $envScope) + [Environment]::SetEnvironmentVariable("SSL_CERT_FILE", $bundlePath, $envScope) + Write-Host " + UV_NATIVE_TLS set; REQUESTS_CA_BUNDLE and SSL_CERT_FILE set to $bundlePath" if (DoHuggingface) { [Environment]::SetEnvironmentVariable("HF_HUB_DISABLE_XET", "1", $envScope) [Environment]::SetEnvironmentVariable("HF_HUB_ETAG_TIMEOUT", "86400", $envScope) @@ -497,6 +629,10 @@ if ($PSCmdlet.ParameterSetName -eq "UseCert") { Write-Host " + HF_HUB_DISABLE_XET and HF Hub timeouts set." } } + if ((DoRuby) -and -not (DoPythonTls)) { + [Environment]::SetEnvironmentVariable("SSL_CERT_FILE", $bundlePath, $envScope) + Write-Host " + SSL_CERT_FILE set to $bundlePath" + } # Only clear the other scope after new vars are set above (so we never leave user with no cert vars on failure). # When setting Machine, remove User-level cert vars so they don't override (User wins over Machine on Windows). if ($envScope -eq "Machine") { @@ -507,6 +643,9 @@ if ($PSCmdlet.ParameterSetName -eq "UseCert") { if (DoPythonTls) { [Environment]::SetEnvironmentVariable("UV_NATIVE_TLS", $null, "User") [Environment]::SetEnvironmentVariable("REQUESTS_CA_BUNDLE", $null, "User") + [Environment]::SetEnvironmentVariable("SSL_CERT_FILE", $null, "User") + } elseif (DoRuby) { + [Environment]::SetEnvironmentVariable("SSL_CERT_FILE", $null, "User") } Write-Host " + Cleared User-level cert vars so Machine settings apply." } @@ -518,7 +657,11 @@ Write-Host "---------------------------------------------------" Write-Host "[4/4] COMPLETE!" Write-Host "" if ($PSCmdlet.ParameterSetName -eq "UseCert") { - Write-Host "Using existing cert at $UseCert. Users must start new terminals to pick up changes." + if (DoOpenSslBundle) { + Write-Host "Using existing cert at $UseCert; Python/Ruby OpenSSL-style clients use bundle $bundlePath. Users must start new terminals to pick up changes." + } else { + Write-Host "Using existing cert at $UseCert. Users must start new terminals to pick up changes." + } } elseif ($isSystemContext -or $isAdmin) { Write-Host "Certificate exported to each user's profile (package-route.pem). User-level env set per user. Users must start new terminals to pick up changes." } else { diff --git a/testing/test_install_certs_windows.ps1 b/testing/test_install_certs_windows.ps1 index 56aeb01..27d471a 100644 --- a/testing/test_install_certs_windows.ps1 +++ b/testing/test_install_certs_windows.ps1 @@ -20,6 +20,24 @@ if (-not (Test-Path -LiteralPath $ValidateScript -PathType Leaf)) { $TempDir = [System.IO.Path]::GetTempPath() + [Guid]::NewGuid().ToString("N").Substring(0, 8) New-Item -ItemType Directory -Path $TempDir -Force | Out-Null +$SavedProgramDataOverride = $env:PACKAGE_REROUTE_PROGRAMDATA +$env:PACKAGE_REROUTE_PROGRAMDATA = $TempDir +$EnvVarsToRestore = @( + "NODE_EXTRA_CA_CERTS", + "NODE_USE_SYSTEM_CA", + "UV_NATIVE_TLS", + "REQUESTS_CA_BUNDLE", + "SSL_CERT_FILE", + "HF_HUB_DISABLE_XET", + "HF_HUB_ETAG_TIMEOUT", + "HF_HUB_DOWNLOAD_TIMEOUT" +) +$SavedUserEnv = @{} +$SavedMachineEnv = @{} +foreach ($var in $EnvVarsToRestore) { + $SavedUserEnv[$var] = [Environment]::GetEnvironmentVariable($var, "User") + $SavedMachineEnv[$var] = [Environment]::GetEnvironmentVariable($var, "Machine") +} try { # Valid PEM with subject CN=test-cert-windows (generated: openssl req -x509 -nodes -newkey rsa:2048 -subj "/CN=test-cert-windows" -days 3650) $CertPath = Join-Path $TempDir "cert.pem" @@ -115,7 +133,7 @@ jXKK5iDphL7LcKir6SLHxmyU339SrjNtTpiSBTU= # Invalid -Package Assert-ExitCode -Expected 1 -ScriptPath $InstallScript -ScriptArguments @("-Package", "foo") - Assert-Stderr -Pattern "ValidateSet|npm|python|huggingface|all" -ScriptPath $InstallScript -ScriptArguments @("-Package", "foo") + Assert-Stderr -Pattern "ValidateSet|npm|python|huggingface|ruby|all" -ScriptPath $InstallScript -ScriptArguments @("-Package", "foo") # -CertName without -ExtractPath (PowerShell requires both in set) Assert-ExitCode -Expected 1 -ScriptPath $InstallScript -ScriptArguments @("-CertName", "X") @@ -151,9 +169,10 @@ jXKK5iDphL7LcKir6SLHxmyU339SrjNtTpiSBTU= Write-Host "" Write-Host "=== install_certs_windows.ps1 Python TLS vs Hugging Face Hub ===" - # -UseCert -Package python: TLS vars set; HF_* left unchanged on Machine + # -UseCert -Package python: TLS vars set to generated bundle; HF_* left unchanged on Machine $savedUv = [Environment]::GetEnvironmentVariable("UV_NATIVE_TLS", "Machine") $savedReq = [Environment]::GetEnvironmentVariable("REQUESTS_CA_BUNDLE", "Machine") + $savedSsl = [Environment]::GetEnvironmentVariable("SSL_CERT_FILE", "Machine") $savedHfXet = [Environment]::GetEnvironmentVariable("HF_HUB_DISABLE_XET", "Machine") $savedHfEtag = [Environment]::GetEnvironmentVariable("HF_HUB_ETAG_TIMEOUT", "Machine") $savedHfDl = [Environment]::GetEnvironmentVariable("HF_HUB_DOWNLOAD_TIMEOUT", "Machine") @@ -166,21 +185,61 @@ jXKK5iDphL7LcKir6SLHxmyU339SrjNtTpiSBTU= } else { $uv = [Environment]::GetEnvironmentVariable("UV_NATIVE_TLS", "Machine") $req = [Environment]::GetEnvironmentVariable("REQUESTS_CA_BUNDLE", "Machine") + $ssl = [Environment]::GetEnvironmentVariable("SSL_CERT_FILE", "Machine") $hfx = [Environment]::GetEnvironmentVariable("HF_HUB_DISABLE_XET", "Machine") $hfe = [Environment]::GetEnvironmentVariable("HF_HUB_ETAG_TIMEOUT", "Machine") $hfd = [Environment]::GetEnvironmentVariable("HF_HUB_DOWNLOAD_TIMEOUT", "Machine") $hfOk = ($hfx -eq $savedHfXet -and $hfe -eq $savedHfEtag -and $hfd -eq $savedHfDl) - if ($uv -eq "1" -and $req -eq $CertPath -and $hfOk) { - Write-Host " OK ($Run): -Package python sets UV_NATIVE_TLS and REQUESTS_CA_BUNDLE; HF_HUB_* unchanged" + if ($uv -eq "1" -and $req -and $req -eq $ssl -and $req -ne $CertPath -and (Test-Path -LiteralPath $req -PathType Leaf) -and $hfOk) { + Write-Host " OK ($Run): -Package python sets UV_NATIVE_TLS, REQUESTS_CA_BUNDLE, SSL_CERT_FILE to generated bundle; HF_HUB_* unchanged" + $script:Pass++ + } else { + Write-Host " FAIL ($Run): UV_NATIVE_TLS='$uv' (expected '1'), REQUESTS_CA_BUNDLE='$req', SSL_CERT_FILE='$ssl' (expected same generated bundle, not '$CertPath'), HF xet/etag/dl='$hfx'/'$hfe'/'$hfd' (expected saved '$savedHfXet'/'$savedHfEtag'/'$savedHfDl')" + $script:Fail++ + } + } + } finally { + [Environment]::SetEnvironmentVariable("UV_NATIVE_TLS", $savedUv, "Machine") + [Environment]::SetEnvironmentVariable("REQUESTS_CA_BUNDLE", $savedReq, "Machine") + [Environment]::SetEnvironmentVariable("SSL_CERT_FILE", $savedSsl, "Machine") + [Environment]::SetEnvironmentVariable("HF_HUB_DISABLE_XET", $savedHfXet, "Machine") + [Environment]::SetEnvironmentVariable("HF_HUB_ETAG_TIMEOUT", $savedHfEtag, "Machine") + [Environment]::SetEnvironmentVariable("HF_HUB_DOWNLOAD_TIMEOUT", $savedHfDl, "Machine") + } + + # -UseCert -Package ruby: Ruby/OpenSSL vars set to generated bundle; Python/HF vars left unchanged on Machine + $savedUv = [Environment]::GetEnvironmentVariable("UV_NATIVE_TLS", "Machine") + $savedReq = [Environment]::GetEnvironmentVariable("REQUESTS_CA_BUNDLE", "Machine") + $savedSsl = [Environment]::GetEnvironmentVariable("SSL_CERT_FILE", "Machine") + $savedHfXet = [Environment]::GetEnvironmentVariable("HF_HUB_DISABLE_XET", "Machine") + $savedHfEtag = [Environment]::GetEnvironmentVariable("HF_HUB_ETAG_TIMEOUT", "Machine") + $savedHfDl = [Environment]::GetEnvironmentVariable("HF_HUB_DOWNLOAD_TIMEOUT", "Machine") + try { + $rRuby = Invoke-ScriptAndGetExitCode -ScriptPath $InstallScript -ScriptArguments @("-UseCert", $CertPath, "-Package", "ruby") + $Run++ + if ($rRuby.ExitCode -ne 0) { + Write-Host " FAIL ($Run): -UseCert -Package ruby expected exit 0, got $($rRuby.ExitCode)" + $script:Fail++ + } else { + $uv = [Environment]::GetEnvironmentVariable("UV_NATIVE_TLS", "Machine") + $req = [Environment]::GetEnvironmentVariable("REQUESTS_CA_BUNDLE", "Machine") + $ssl = [Environment]::GetEnvironmentVariable("SSL_CERT_FILE", "Machine") + $hfx = [Environment]::GetEnvironmentVariable("HF_HUB_DISABLE_XET", "Machine") + $hfe = [Environment]::GetEnvironmentVariable("HF_HUB_ETAG_TIMEOUT", "Machine") + $hfd = [Environment]::GetEnvironmentVariable("HF_HUB_DOWNLOAD_TIMEOUT", "Machine") + $otherOk = ($uv -eq $savedUv -and $req -eq $savedReq -and $hfx -eq $savedHfXet -and $hfe -eq $savedHfEtag -and $hfd -eq $savedHfDl) + if ($ssl -and $ssl -ne $CertPath -and (Test-Path -LiteralPath $ssl -PathType Leaf) -and $otherOk) { + Write-Host " OK ($Run): -Package ruby sets SSL_CERT_FILE to generated bundle and leaves Python/HF vars unchanged" $script:Pass++ } else { - Write-Host " FAIL ($Run): UV_NATIVE_TLS='$uv' (expected '1'), REQUESTS_CA_BUNDLE='$req' (expected '$CertPath'), HF xet/etag/dl='$hfx'/'$hfe'/'$hfd' (expected saved '$savedHfXet'/'$savedHfEtag'/'$savedHfDl')" + Write-Host " FAIL ($Run): SSL_CERT_FILE='$ssl' (expected generated bundle, not '$CertPath'), UV='$uv' REQ='$req' HF='$hfx'/'$hfe'/'$hfd'" $script:Fail++ } } } finally { [Environment]::SetEnvironmentVariable("UV_NATIVE_TLS", $savedUv, "Machine") [Environment]::SetEnvironmentVariable("REQUESTS_CA_BUNDLE", $savedReq, "Machine") + [Environment]::SetEnvironmentVariable("SSL_CERT_FILE", $savedSsl, "Machine") [Environment]::SetEnvironmentVariable("HF_HUB_DISABLE_XET", $savedHfXet, "Machine") [Environment]::SetEnvironmentVariable("HF_HUB_ETAG_TIMEOUT", $savedHfEtag, "Machine") [Environment]::SetEnvironmentVariable("HF_HUB_DOWNLOAD_TIMEOUT", $savedHfDl, "Machine") @@ -189,6 +248,7 @@ jXKK5iDphL7LcKir6SLHxmyU339SrjNtTpiSBTU= # -UseCert -Package huggingface: Python TLS + HF Hub on Machine $savedUv = [Environment]::GetEnvironmentVariable("UV_NATIVE_TLS", "Machine") $savedReq = [Environment]::GetEnvironmentVariable("REQUESTS_CA_BUNDLE", "Machine") + $savedSsl = [Environment]::GetEnvironmentVariable("SSL_CERT_FILE", "Machine") $savedHfXet = [Environment]::GetEnvironmentVariable("HF_HUB_DISABLE_XET", "Machine") $savedHfEtag = [Environment]::GetEnvironmentVariable("HF_HUB_ETAG_TIMEOUT", "Machine") $savedHfDl = [Environment]::GetEnvironmentVariable("HF_HUB_DOWNLOAD_TIMEOUT", "Machine") @@ -201,11 +261,12 @@ jXKK5iDphL7LcKir6SLHxmyU339SrjNtTpiSBTU= } else { $uv = [Environment]::GetEnvironmentVariable("UV_NATIVE_TLS", "Machine") $req = [Environment]::GetEnvironmentVariable("REQUESTS_CA_BUNDLE", "Machine") + $ssl = [Environment]::GetEnvironmentVariable("SSL_CERT_FILE", "Machine") $hfx = [Environment]::GetEnvironmentVariable("HF_HUB_DISABLE_XET", "Machine") $hfe = [Environment]::GetEnvironmentVariable("HF_HUB_ETAG_TIMEOUT", "Machine") $hfd = [Environment]::GetEnvironmentVariable("HF_HUB_DOWNLOAD_TIMEOUT", "Machine") - if ($uv -eq "1" -and $req -eq $CertPath -and $hfx -eq "1" -and $hfe -eq "86400" -and $hfd -eq "86400") { - Write-Host " OK ($Run): -Package huggingface sets UV_NATIVE_TLS, REQUESTS_CA_BUNDLE, HF_HUB_*" + if ($uv -eq "1" -and $req -and $req -eq $ssl -and $req -ne $CertPath -and (Test-Path -LiteralPath $req -PathType Leaf) -and $hfx -eq "1" -and $hfe -eq "86400" -and $hfd -eq "86400") { + Write-Host " OK ($Run): -Package huggingface sets UV_NATIVE_TLS, REQUESTS_CA_BUNDLE, SSL_CERT_FILE, HF_HUB_*" $script:Pass++ } else { Write-Host " FAIL ($Run): huggingface package env mismatch" @@ -215,6 +276,7 @@ jXKK5iDphL7LcKir6SLHxmyU339SrjNtTpiSBTU= } finally { [Environment]::SetEnvironmentVariable("UV_NATIVE_TLS", $savedUv, "Machine") [Environment]::SetEnvironmentVariable("REQUESTS_CA_BUNDLE", $savedReq, "Machine") + [Environment]::SetEnvironmentVariable("SSL_CERT_FILE", $savedSsl, "Machine") [Environment]::SetEnvironmentVariable("HF_HUB_DISABLE_XET", $savedHfXet, "Machine") [Environment]::SetEnvironmentVariable("HF_HUB_ETAG_TIMEOUT", $savedHfEtag, "Machine") [Environment]::SetEnvironmentVariable("HF_HUB_DOWNLOAD_TIMEOUT", $savedHfDl, "Machine") @@ -225,6 +287,7 @@ jXKK5iDphL7LcKir6SLHxmyU339SrjNtTpiSBTU= $savedNodeSys = [Environment]::GetEnvironmentVariable("NODE_USE_SYSTEM_CA", "Machine") $savedUv = [Environment]::GetEnvironmentVariable("UV_NATIVE_TLS", "Machine") $savedReq = [Environment]::GetEnvironmentVariable("REQUESTS_CA_BUNDLE", "Machine") + $savedSsl = [Environment]::GetEnvironmentVariable("SSL_CERT_FILE", "Machine") $savedHfXet = [Environment]::GetEnvironmentVariable("HF_HUB_DISABLE_XET", "Machine") $savedHfEtag = [Environment]::GetEnvironmentVariable("HF_HUB_ETAG_TIMEOUT", "Machine") $savedHfDl = [Environment]::GetEnvironmentVariable("HF_HUB_DOWNLOAD_TIMEOUT", "Machine") @@ -237,16 +300,17 @@ jXKK5iDphL7LcKir6SLHxmyU339SrjNtTpiSBTU= } else { $uvAll = [Environment]::GetEnvironmentVariable("UV_NATIVE_TLS", "Machine") $reqAll = [Environment]::GetEnvironmentVariable("REQUESTS_CA_BUNDLE", "Machine") + $sslAll = [Environment]::GetEnvironmentVariable("SSL_CERT_FILE", "Machine") $nodeAll = [Environment]::GetEnvironmentVariable("NODE_EXTRA_CA_CERTS", "Machine") $nodeSysAll = [Environment]::GetEnvironmentVariable("NODE_USE_SYSTEM_CA", "Machine") $hfxAll = [Environment]::GetEnvironmentVariable("HF_HUB_DISABLE_XET", "Machine") $hfeAll = [Environment]::GetEnvironmentVariable("HF_HUB_ETAG_TIMEOUT", "Machine") $hfdAll = [Environment]::GetEnvironmentVariable("HF_HUB_DOWNLOAD_TIMEOUT", "Machine") - if ($nodeSysAll -eq "1" -and $nodeAll -eq $CertPath -and $uvAll -eq "1" -and $reqAll -eq $CertPath -and $hfxAll -eq "1" -and $hfeAll -eq "86400" -and $hfdAll -eq "86400") { - Write-Host " OK ($Run): -Package all sets NODE_*, UV_NATIVE_TLS, REQUESTS_CA_BUNDLE, HF_HUB_*" + if ($nodeSysAll -eq "1" -and $nodeAll -eq $CertPath -and $uvAll -eq "1" -and $reqAll -and $reqAll -eq $sslAll -and $reqAll -ne $CertPath -and (Test-Path -LiteralPath $reqAll -PathType Leaf) -and $hfxAll -eq "1" -and $hfeAll -eq "86400" -and $hfdAll -eq "86400") { + Write-Host " OK ($Run): -Package all sets NODE_*, UV_NATIVE_TLS, REQUESTS_CA_BUNDLE, SSL_CERT_FILE, HF_HUB_*" $script:Pass++ } else { - Write-Host " FAIL ($Run): NODE_USE_SYSTEM_CA='$nodeSysAll' NODE_EXTRA_CA_CERTS='$nodeAll' UV_NATIVE_TLS='$uvAll' REQUESTS_CA_BUNDLE='$reqAll' HF='$hfxAll'/'$hfeAll'/'$hfdAll'" + Write-Host " FAIL ($Run): NODE_USE_SYSTEM_CA='$nodeSysAll' NODE_EXTRA_CA_CERTS='$nodeAll' UV_NATIVE_TLS='$uvAll' REQUESTS_CA_BUNDLE='$reqAll' SSL_CERT_FILE='$sslAll' HF='$hfxAll'/'$hfeAll'/'$hfdAll'" $script:Fail++ } } @@ -255,6 +319,7 @@ jXKK5iDphL7LcKir6SLHxmyU339SrjNtTpiSBTU= [Environment]::SetEnvironmentVariable("NODE_USE_SYSTEM_CA", $savedNodeSys, "Machine") [Environment]::SetEnvironmentVariable("UV_NATIVE_TLS", $savedUv, "Machine") [Environment]::SetEnvironmentVariable("REQUESTS_CA_BUNDLE", $savedReq, "Machine") + [Environment]::SetEnvironmentVariable("SSL_CERT_FILE", $savedSsl, "Machine") [Environment]::SetEnvironmentVariable("HF_HUB_DISABLE_XET", $savedHfXet, "Machine") [Environment]::SetEnvironmentVariable("HF_HUB_ETAG_TIMEOUT", $savedHfEtag, "Machine") [Environment]::SetEnvironmentVariable("HF_HUB_DOWNLOAD_TIMEOUT", $savedHfDl, "Machine") @@ -320,6 +385,10 @@ jXKK5iDphL7LcKir6SLHxmyU339SrjNtTpiSBTU= # Current user env: no paths set → WARN, exit 0 (use -Command so -ExpectedSubject is passed) $Run++ + foreach ($var in @("NODE_EXTRA_CA_CERTS", "REQUESTS_CA_BUNDLE", "SSL_CERT_FILE")) { + [Environment]::SetEnvironmentVariable($var, $null, "User") + [Environment]::SetEnvironmentVariable($var, $null, "Machine") + } $scriptEscaped = $ValidateScript -replace "'", "''" $cmd = "& '$scriptEscaped' -ExpectedSubject 'test-cert'; exit `$LASTEXITCODE" $allArgs = @("-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", $cmd) @@ -378,6 +447,11 @@ jXKK5iDphL7LcKir6SLHxmyU339SrjNtTpiSBTU= if ($Fail -gt 0) { exit 1 } exit 0 } finally { + foreach ($var in $EnvVarsToRestore) { + [Environment]::SetEnvironmentVariable($var, $SavedUserEnv[$var], "User") + [Environment]::SetEnvironmentVariable($var, $SavedMachineEnv[$var], "Machine") + } + $env:PACKAGE_REROUTE_PROGRAMDATA = $SavedProgramDataOverride if (Test-Path -LiteralPath $TempDir -PathType Container) { Remove-Item -LiteralPath $TempDir -Recurse -Force -ErrorAction SilentlyContinue } diff --git a/validate_install_windows.ps1 b/validate_install_windows.ps1 index 7b7c93e..c170bdf 100644 --- a/validate_install_windows.ps1 +++ b/validate_install_windows.ps1 @@ -92,7 +92,7 @@ function Validate-Pem { # Get effective cert paths for current user (User overrides Machine on Windows). function Get-CurrentUserCertPaths { $paths = @() - foreach ($var in @("NODE_EXTRA_CA_CERTS", "REQUESTS_CA_BUNDLE")) { + foreach ($var in @("NODE_EXTRA_CA_CERTS", "REQUESTS_CA_BUNDLE", "SSL_CERT_FILE")) { $val = [Environment]::GetEnvironmentVariable($var, "User") if ([string]::IsNullOrWhiteSpace($val)) { $val = [Environment]::GetEnvironmentVariable($var, "Machine") } if (-not [string]::IsNullOrWhiteSpace($val)) { @@ -136,7 +136,8 @@ function Get-OtherUserCertPaths { if (Test-Path -LiteralPath $keyPath) { $nodePath = (Get-ItemProperty -Path $keyPath -Name "NODE_EXTRA_CA_CERTS" -ErrorAction SilentlyContinue).NODE_EXTRA_CA_CERTS $pipPath = (Get-ItemProperty -Path $keyPath -Name "REQUESTS_CA_BUNDLE" -ErrorAction SilentlyContinue).REQUESTS_CA_BUNDLE - foreach ($p in @($nodePath, $pipPath)) { + $sslPath = (Get-ItemProperty -Path $keyPath -Name "SSL_CERT_FILE" -ErrorAction SilentlyContinue).SSL_CERT_FILE + foreach ($p in @($nodePath, $pipPath, $sslPath)) { if (-not [string]::IsNullOrWhiteSpace($p)) { $p = $p.Trim().Trim('"').Trim("'") if ($p -and $paths -notcontains $p) { $paths += $p } @@ -163,7 +164,7 @@ if ($AllUsers) { $userHome = $ud.FullName $paths = Get-OtherUserCertPaths -ProfilePath $userHome if ($paths.Count -eq 0) { - Write-Host " SKIP: user $($ud.Name) has no NODE_EXTRA_CA_CERTS or REQUESTS_CA_BUNDLE set" + Write-Host " SKIP: user $($ud.Name) has no NODE_EXTRA_CA_CERTS, REQUESTS_CA_BUNDLE, or SSL_CERT_FILE set" continue } Write-Host " Checking user $($ud.Name)..." @@ -175,7 +176,7 @@ if ($AllUsers) { Write-Host "Validating current user config (env) and cert path(s)..." $paths = Get-CurrentUserCertPaths if ($paths.Count -eq 0) { - Write-Host " WARN: no NODE_EXTRA_CA_CERTS or REQUESTS_CA_BUNDLE set for current user" + Write-Host " WARN: no NODE_EXTRA_CA_CERTS, REQUESTS_CA_BUNDLE, or SSL_CERT_FILE set for current user" } else { foreach ($p in $paths) { Validate-Pem -Path $p | Out-Null