Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions .github/actions/upgrade-ca-certificates/emudhra_intermediates.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,33 @@
$EmudhraIntermediatesPath = Join-Path $PSScriptRoot 'impacted-intermediates.json'
$EmudhraIntermediatesCount = 16

# Uppercase hex SHA-256 of the certificate's DER encoding. Computed directly
# because X509Certificate.GetCertHashString(HashAlgorithmName) does not exist
# in Windows PowerShell 5.1 (.NET Framework).
function Get-CertificateSha256([System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert) {
$sha256 = [System.Security.Cryptography.SHA256]::Create()
try {
return [System.BitConverter]::ToString($sha256.ComputeHash($Cert.RawData)).Replace('-', '')
} finally {
$sha256.Dispose()
}
}

# Returns the certificates as X509Certificate2 objects; callers dispose them.
# Works on both Windows PowerShell 5.1 and PowerShell 7+.
function Get-EmudhraIntermediates {
$entries = @(Get-Content -LiteralPath $EmudhraIntermediatesPath -Raw | ConvertFrom-Json)
# Windows PowerShell 5.1's ConvertFrom-Json emits a JSON array as a single
# object; piping it through ForEach-Object enumerates it on both versions.
$parsed = Get-Content -LiteralPath $EmudhraIntermediatesPath -Raw | ConvertFrom-Json
$entries = @($parsed | ForEach-Object { $_ })
if ($entries.Count -ne $EmudhraIntermediatesCount) {
throw "Expected $EmudhraIntermediatesCount certificates in $EmudhraIntermediatesPath, found $($entries.Count)."
}
foreach ($entry in $entries) {
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::CreateFromPem($entry.pem)
if ($cert.GetCertHashString('SHA256') -ne $entry.sha256.ToUpperInvariant()) {
# X509Certificate2.CreateFromPem is .NET 5+ only; decode the PEM body manually.
$der = [System.Convert]::FromBase64String(($entry.pem -replace '-----[A-Z ]+-----', '' -replace '\s', ''))
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($der)
if ((Get-CertificateSha256 $cert) -ne $entry.sha256.ToUpperInvariant()) {
$cert.Dispose()
throw "SHA-256 mismatch for $($entry.name) (issued by $($entry.issued_by))."
}
Expand All @@ -40,7 +58,10 @@ function Test-ExplicitlyDistrusted([System.Security.Cryptography.X509Certificate
$chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
try {
$chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck
$chain.ChainPolicy.DisableCertificateDownloads = $true
# DisableCertificateDownloads does not exist on .NET Framework (Windows PowerShell 5.1).
if ($chain.ChainPolicy | Get-Member -Name DisableCertificateDownloads) {
$chain.ChainPolicy.DisableCertificateDownloads = $true
}
[void]$chain.Build($Cert)
$statuses = @($chain.ChainStatus) + @($chain.ChainElements | ForEach-Object { $_.ChainElementStatus })
return [bool]($statuses | Where-Object { $_.Status.HasFlag($explicitDistrust) })
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/runner-trust-probe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ jobs:

- name: Probe Windows OS trust store
if: ${{ runner.os == 'Windows' }}
shell: pwsh
shell: powershell
run: ./ci/tools/runner_trust_probe.ps1

- name: Probe Node 20 action runtime (informational)
Expand Down
19 changes: 14 additions & 5 deletions ci/tools/runner_trust_probe.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#
# runner_trust_probe.ps1 -- probe the Windows certificate store on a CI runner
# for the compromised eMudhra intermediates and their Sectigo cross-signs.
# Compatible with Windows PowerShell 5.1 (`shell: powershell`) and PowerShell 7+.

$ErrorActionPreference = 'Stop'

Expand All @@ -21,7 +22,8 @@ $SummaryFile = $env:GITHUB_STEP_SUMMARY

function Append-Summary([string]$Line) {
if ($SummaryFile) {
Add-Content -Path $SummaryFile -Value $Line -Encoding utf8
# Windows PowerShell 5.1's `-Encoding utf8` emits a BOM; write BOM-less UTF-8 explicitly.
[System.IO.File]::AppendAllText($SummaryFile, "$Line`n", [System.Text.UTF8Encoding]::new($false))
}
}

Expand All @@ -40,9 +42,9 @@ Append-Summary "| --- | --- | --- |"
# 1. Root anchor state (informational). Microsoft's fix blocks the
# intermediates and leaves the root in place, so this is not a pass criterion.
$rootCerts = @(Get-ChildItem -Path 'Cert:\LocalMachine\Root' -ErrorAction Stop)
$inRoot = [bool]($rootCerts | Where-Object { $_.GetCertHashString('SHA256') -eq $EmSignG3Hex })
$inRoot = [bool]($rootCerts | Where-Object { (Get-CertificateSha256 $_) -eq $EmSignG3Hex })
$inDisallowed = [bool](Get-ChildItem -Path 'Cert:\LocalMachine\Disallowed' -ErrorAction SilentlyContinue |
Where-Object { $_.GetCertHashString('SHA256') -eq $EmSignG3Hex })
Where-Object { (Get-CertificateSha256 $_) -eq $EmSignG3Hex })
Record-Check 'emSign ECC Root CA - G3 store state' 'INFO' "LocalMachine\Root=$inRoot ($($rootCerts.Count) certs), LocalMachine\Disallowed=$inDisallowed"

# 2. All compromised intermediates and cross-signs explicitly distrusted (gating)
Expand All @@ -59,17 +61,24 @@ if ($notDistrusted.Count -eq 0) {
$GatingFailures++
}

# 3. OS-store client (Invoke-WebRequest) rejects each test endpoint (only a successful connection fails the job)
# 3. OS-store client (Invoke-WebRequest) rejects each test endpoint (only a successful connection fails the job).
# -SkipHttpErrorCheck is PowerShell 7+ only, so HTTP error statuses are caught below instead:
# any response at all means the TLS handshake (and thus the certificate chain) succeeded.
$progTmp = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
try {
foreach ($endpoint in $TestEndpoints) {
$checkName = "Invoke-WebRequest rejects ``$endpoint``"
try {
$res = Invoke-WebRequest -Uri $endpoint -UseBasicParsing -SkipHttpErrorCheck -TimeoutSec 15
$res = Invoke-WebRequest -Uri $endpoint -UseBasicParsing -TimeoutSec 15
Record-Check $checkName 'FAIL' "Connection succeeded with HTTP $($res.StatusCode) (certificate was trusted)"
$GatingFailures++
} catch {
if ($null -ne $_.Exception.Response) {
Record-Check $checkName 'FAIL' "Connection succeeded with HTTP $([int]$_.Exception.Response.StatusCode) (certificate was trusted)"
$GatingFailures++
continue
}
$certRejected = $false
$hostnameMismatchOnly = $false
$ex = $_.Exception
Expand Down
Loading