diff --git a/install.ps1 b/install.ps1 index 0a1a428755..f52d1b7a04 100644 --- a/install.ps1 +++ b/install.ps1 @@ -2223,6 +2223,24 @@ exit 0 return $null } + # Same trust boundary as the VC++ runtime in studio/setup.ps1: $full moves per patch + # release, so there is no SHA-256 to pin and the publisher is what we can check. + # Inspection itself can fail (antivirus quarantining the download first), and the + # script-wide 'Stop' would let that escape the function, skipping the $null fallback + # and leaving the executable behind. Unreadable is unverified, so it takes the same + # route as a bad signature. + $sig = $null + try { $sig = Get-AuthenticodeSignature -LiteralPath $dest } catch { $sig = $null } + if ($null -eq $sig -or + $sig.Status -ne [System.Management.Automation.SignatureStatus]::Valid -or + $null -eq $sig.SignerCertificate -or + $sig.SignerCertificate.Subject -notmatch '(^|,\s*)O="?Python Software Foundation"?(,|$)') { + $sigStatus = if ($null -eq $sig) { "could not be read" } else { $sig.Status } + substep "python.org installer is not validly signed by the Python Software Foundation (signature status: $sigStatus); not running it." "Yellow" + Remove-Item -LiteralPath $dest -Force -ErrorAction SilentlyContinue + return $null + } + # Per-user install => no UAC. PrependPath puts python + py on PATH; # Include_launcher installs py.exe (preferred by Find-CompatiblePython). substep "installing Python $full (silent, per-user)..." diff --git a/studio/setup.ps1 b/studio/setup.ps1 index a37d0faf5e..4a1ab6fa6b 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -1514,6 +1514,16 @@ function Ensure-VCRedist { } catch { $_prevProtocol = $null } try { Invoke-WebRequest -Uri $url -OutFile $dst -UseBasicParsing -TimeoutSec 300 + # HTTPS secures the transfer, not the payload, and this runs with the setup + # process's privileges. The evergreen URL rules out a SHA-256 pin (the bytes + # change with every VS servicing update), so check the publisher. Status alone + # is not enough: any trusted CA's code-signing cert passes it. + $sig = Get-AuthenticodeSignature -LiteralPath $dst + if ($sig.Status -ne [System.Management.Automation.SignatureStatus]::Valid -or + $null -eq $sig.SignerCertificate -or + $sig.SignerCertificate.Subject -notmatch '(^|,\s*)O="?Microsoft Corporation"?(,|$)') { + throw "the downloaded VC++ runtime is not validly signed by Microsoft (signature status: $($sig.Status))" + } $p = Start-Process -FilePath $dst -ArgumentList '/quiet', '/norestart' -Wait -PassThru # 3010 = success, reboot required; usable either way. if ($p.ExitCode -notin @(0, 3010)) { diff --git a/tests/python/test_windows_vcredist_download_tls.py b/tests/python/test_windows_vcredist_download_tls.py index 9fb1c697e2..146a9ae104 100644 --- a/tests/python/test_windows_vcredist_download_tls.py +++ b/tests/python/test_windows_vcredist_download_tls.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. -"""The direct VC++ runtime download must negotiate TLS 1.2 on legacy protocol defaults.""" +"""The direct VC++ runtime download must negotiate TLS 1.2 and run only Microsoft's binary.""" from __future__ import annotations @@ -27,6 +27,20 @@ def _download_block() -> str: return source[start:end] +def test_the_download_is_verified_as_microsoft_signed_before_it_runs(): + # No pwsh needed: Get-AuthenticodeSignature is Windows-only, so the ordering of the three + # steps in the real block is the thing to hold still. A verification placed after + # Start-Process, or one that only checks Status, would still "pass" on a swapped binary. + block = _download_block() + download = block.index("Invoke-WebRequest") + verify = block.index("Get-AuthenticodeSignature", download) + execute = block.index("Start-Process", verify) + assert download < verify < execute + assert "SignatureStatus]::Valid" in block + # Loose on the quoting, since an RDN value may arrive quoted, strict on the publisher. + assert "Microsoft Corporation" in block + + def _script(starting_protocol: str) -> str: # Start from a non-zero set that lacks Tls12. Tls13 is the only such value modern .NET # accepts, and it stands in for the legacy Ssl3/Tls default of Windows PowerShell 5.1.