Skip to content

Commit cc2dc81

Browse files
committed
CI: accept a missing trust anchor in the Windows runner trust probe
The hosted Windows images removed the emSign roots instead of enabling Microsoft's disallowed CTL, so chain building reports UntrustedRoot or PartialChain rather than ExplicitDistrust for the compromised intermediates. Pass the check when each intermediate is explicitly distrusted or its chain no longer reaches a trusted root.
1 parent e01e453 commit cc2dc81

2 files changed

Lines changed: 55 additions & 5 deletions

File tree

‎.github/actions/upgrade-ca-certificates/emudhra_intermediates.ps1‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,42 @@ function Test-ExplicitlyDistrusted([System.Security.Cryptography.X509Certificate
6969
$chain.Dispose()
7070
}
7171
}
72+
73+
# Classifies how Windows chain building treats the certificate, using only the
74+
# local trust state (no revocation, no AIA downloads):
75+
# ExplicitDistrust : in LocalMachine\Disallowed or Microsoft's disallowed list
76+
# NoTrustAnchor : the chain no longer ends in a trusted root (UntrustedRoot
77+
# or PartialChain), e.g. because the root was removed
78+
# Trusted : the chain builds cleanly, so the compromised key is usable
79+
# Indeterminate : the build failed for some other reason; never a pass
80+
# Verification time is pinned (the instant used by runner_trust_probe.sh) so an
81+
# expired certificate cannot masquerade as untrusted.
82+
function Get-ChainTrustState([System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert) {
83+
$explicitDistrust = [System.Security.Cryptography.X509Certificates.X509ChainStatusFlags]::ExplicitDistrust
84+
$noAnchor = [System.Security.Cryptography.X509Certificates.X509ChainStatusFlags]::UntrustedRoot -bor
85+
[System.Security.Cryptography.X509Certificates.X509ChainStatusFlags]::PartialChain
86+
$chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
87+
try {
88+
$chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck
89+
$chain.ChainPolicy.VerificationTime = [DateTime]::new(2026, 10, 23, 0, 0, 0, [DateTimeKind]::Utc)
90+
# DisableCertificateDownloads does not exist on .NET Framework (Windows PowerShell 5.1).
91+
if ($chain.ChainPolicy | Get-Member -Name DisableCertificateDownloads) {
92+
$chain.ChainPolicy.DisableCertificateDownloads = $true
93+
}
94+
$built = $chain.Build($Cert)
95+
$statuses = @($chain.ChainStatus) + @($chain.ChainElements | ForEach-Object { $_.ChainElementStatus })
96+
$names = @($statuses | ForEach-Object { $_.Status.ToString() } | Where-Object { $_ -ne 'NoError' } | Sort-Object -Unique)
97+
if ($statuses | Where-Object { $_.Status.HasFlag($explicitDistrust) }) {
98+
$state = 'ExplicitDistrust'
99+
} elseif ($built) {
100+
$state = 'Trusted'
101+
} elseif ($statuses | Where-Object { ($_.Status -band $noAnchor) -ne 0 }) {
102+
$state = 'NoTrustAnchor'
103+
} else {
104+
$state = 'Indeterminate'
105+
}
106+
return [pscustomobject]@{ State = $state; Statuses = ($names -join ',') }
107+
} finally {
108+
$chain.Dispose()
109+
}
110+
}

‎ci/tools/runner_trust_probe.ps1‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,28 @@ $inDisallowed = [bool](Get-ChildItem -Path 'Cert:\LocalMachine\Disallowed' -Erro
4747
Where-Object { (Get-CertificateSha256 $_) -eq $EmSignG3Hex })
4848
Record-Check 'emSign ECC Root CA - G3 store state' 'INFO' "LocalMachine\Root=$inRoot ($($rootCerts.Count) certs), LocalMachine\Disallowed=$inDisallowed"
4949

50-
# 2. All compromised intermediates and cross-signs explicitly distrusted (gating)
50+
# 2. No compromised intermediate or cross-sign is usable as a trust path (gating).
51+
# Pass = explicitly distrusted, or the chain no longer reaches a trusted root
52+
# (the hosted images have removed the roots rather than enabling the disallowed CTL).
5153
$certs = @(Get-EmudhraIntermediates)
54+
$usable = @()
55+
$states = @()
5256
try {
53-
$notDistrusted = @($certs | Where-Object { -not (Test-ExplicitlyDistrusted $_) } | ForEach-Object { $_.EmudhraLabel })
57+
foreach ($c in $certs) {
58+
$r = Get-ChainTrustState $c
59+
$states += $r.State
60+
if ($r.State -notin @('ExplicitDistrust', 'NoTrustAnchor')) {
61+
$usable += "$($c.EmudhraLabel) [$($r.State): $($r.Statuses)]"
62+
}
63+
}
5464
} finally {
5565
$certs | ForEach-Object { $_.Dispose() }
5666
}
57-
if ($notDistrusted.Count -eq 0) {
58-
Record-Check 'Compromised intermediates distrusted' 'PASS' "All $($certs.Count) explicitly distrusted"
67+
$counts = ($states | Group-Object | ForEach-Object { "$($_.Name)=$($_.Count)" }) -join ', '
68+
if ($usable.Count -eq 0) {
69+
Record-Check 'Compromised intermediates distrusted' 'PASS' "All $($certs.Count) rejected ($counts)"
5970
} else {
60-
Record-Check 'Compromised intermediates distrusted' 'FAIL' "$($notDistrusted.Count) of $($certs.Count) not explicitly distrusted: $($notDistrusted -join '; ')"
71+
Record-Check 'Compromised intermediates distrusted' 'FAIL' "$($usable.Count) of $($certs.Count) usable or indeterminate: $($usable -join '; ')"
6172
$GatingFailures++
6273
}
6374

0 commit comments

Comments
 (0)