@@ -69,3 +69,56 @@ 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, and
77+ # UntrustedRoot / PartialChain are the only problems
78+ # reported, e.g. because the root was removed
79+ # Trusted : the chain builds cleanly, so the compromised key is usable
80+ # Indeterminate : the build failed for some other reason; never a pass
81+ # Verification time is pinned (the instant used by runner_trust_probe.sh) so an
82+ # expired certificate cannot masquerade as untrusted.
83+ # A PartialChain only shows that the root is untrusted if the issuers are
84+ # available to the build, so callers pass the non-root issuers in
85+ # $ExtraCertificates. Issuers that are roots need not be supplied: a missing
86+ # root is exactly the condition this function reports as NoTrustAnchor.
87+ function Get-ChainTrustState (
88+ [System.Security.Cryptography.X509Certificates.X509Certificate2 ]$Cert ,
89+ [System.Security.Cryptography.X509Certificates.X509Certificate2 []]$ExtraCertificates = @ ()
90+ ) {
91+ $explicitDistrust = [System.Security.Cryptography.X509Certificates.X509ChainStatusFlags ]::ExplicitDistrust
92+ $noAnchor = [int ]([System.Security.Cryptography.X509Certificates.X509ChainStatusFlags ]::UntrustedRoot -bor
93+ [System.Security.Cryptography.X509Certificates.X509ChainStatusFlags ]::PartialChain)
94+ $chain = [System.Security.Cryptography.X509Certificates.X509Chain ]::new()
95+ try {
96+ foreach ($extra in $ExtraCertificates ) {
97+ [void ]$chain.ChainPolicy.ExtraStore.Add ($extra )
98+ }
99+ $chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode ]::NoCheck
100+ $chain.ChainPolicy.VerificationTime = [DateTime ]::new(2026 , 10 , 23 , 0 , 0 , 0 , [DateTimeKind ]::Utc)
101+ # DisableCertificateDownloads does not exist on .NET Framework (Windows PowerShell 5.1).
102+ if ($chain.ChainPolicy | Get-Member - Name DisableCertificateDownloads) {
103+ $chain.ChainPolicy.DisableCertificateDownloads = $true
104+ }
105+ $built = $chain.Build ($Cert )
106+ $statuses = @ ($chain.ChainStatus ) + @ ($chain.ChainElements | ForEach-Object { $_.ChainElementStatus })
107+ $names = @ ($statuses | ForEach-Object { $_.Status.ToString () } | Where-Object { $_ -ne ' NoError' } | Sort-Object - Unique)
108+ if ($statuses | Where-Object { $_.Status.HasFlag ($explicitDistrust ) }) {
109+ $state = ' ExplicitDistrust'
110+ } elseif ($built ) {
111+ $state = ' Trusted'
112+ } elseif (
113+ ($statuses | Where-Object { ([int ]$_.Status -band $noAnchor ) -ne 0 }) -and
114+ -not ($statuses | Where-Object { ([int ]$_.Status -band (-bnot $noAnchor )) -ne 0 })
115+ ) {
116+ $state = ' NoTrustAnchor'
117+ } else {
118+ $state = ' Indeterminate'
119+ }
120+ return [pscustomobject ]@ { State = $state ; Statuses = ($names -join ' ,' ) }
121+ } finally {
122+ $chain.Dispose ()
123+ }
124+ }
0 commit comments