@@ -47,62 +47,107 @@ $inDisallowed = [bool](Get-ChildItem -Path 'Cert:\LocalMachine\Disallowed' -Erro
4747 Where-Object { (Get-CertificateSha256 $_ ) -eq $EmSignG3Hex })
4848Record- 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+ # Supply the non-root issuers so a missing parent is not mistaken for a missing trust
55+ # anchor: emSign Root TLS CA - G3 comes with the vendored host chains, and
56+ # emSign Root CS CA - G3 (cross-signed by emSign ECC Root CA - G3) with the issuers.
57+ $issuerCerts = @ ()
58+ foreach ($dirName in ' runner_trust_probe_chains' , ' runner_trust_probe_issuers' ) {
59+ $found = @ (
60+ Get-ChildItem - LiteralPath (Join-Path $PSScriptRoot $dirName ) - Filter ' *.pem' |
61+ ForEach-Object {
62+ [regex ]::Matches((Get-Content - LiteralPath $_.FullName - Raw), ' -----BEGIN CERTIFICATE-----([^-]+)-----END CERTIFICATE-----' ) |
63+ ForEach-Object {
64+ [System.Security.Cryptography.X509Certificates.X509Certificate2 ]::new([System.Convert ]::FromBase64String(($_.Groups [1 ].Value -replace ' \s' , ' ' )))
65+ }
66+ }
67+ )
68+ if ($found.Count -eq 0 ) {
69+ throw " No certificates found in $PSScriptRoot \$dirName ."
70+ }
71+ $issuerCerts += $found
72+ }
73+ $usable = @ ()
74+ $states = @ ()
75+ $statusNames = @ ()
5276try {
53- $notDistrusted = @ ($certs | Where-Object { -not (Test-ExplicitlyDistrusted $_ ) } | ForEach-Object { $_.EmudhraLabel })
77+ foreach ($c in $certs ) {
78+ $r = Get-ChainTrustState $c $issuerCerts
79+ $states += $r.State
80+ $statusNames += $r.Statuses -split ' ,' | Where-Object { $_ }
81+ if ($r.State -notin @ (' ExplicitDistrust' , ' NoTrustAnchor' )) {
82+ $usable += " $ ( $c.EmudhraLabel ) [$ ( $r.State ) : $ ( $r.Statuses ) ]"
83+ }
84+ }
5485} finally {
5586 $certs | ForEach-Object { $_.Dispose () }
87+ $issuerCerts | ForEach-Object { $_.Dispose () }
5688}
57- if ($notDistrusted.Count -eq 0 ) {
58- Record- Check ' Compromised intermediates distrusted' ' PASS' " All $ ( $certs.Count ) explicitly distrusted"
89+ $counts = ($states | Group-Object | ForEach-Object { " $ ( $_.Name ) =$ ( $_.Count ) " }) -join ' , '
90+ $flags = ($statusNames | Sort-Object - Unique) -join ' ,'
91+ if ($usable.Count -eq 0 ) {
92+ Record- Check ' Compromised intermediates distrusted' ' PASS' " All $ ( $certs.Count ) rejected ($counts ; chain status: $flags )"
5993} else {
60- Record- Check ' Compromised intermediates distrusted' ' FAIL' " $ ( $notDistrusted .Count ) of $ ( $certs.Count ) not explicitly distrusted : $ ( $notDistrusted -join ' ; ' ) "
94+ Record- Check ' Compromised intermediates distrusted' ' FAIL' " $ ( $usable .Count ) of $ ( $certs.Count ) usable or indeterminate : $ ( $usable -join ' ; ' ) "
6195 $GatingFailures ++
6296}
6397
64- # 3. OS-store client (Invoke-WebRequest) rejects each test endpoint (only a successful connection fails the job).
98+ # 3. OS-store client (Invoke-WebRequest) rejects each test endpoint (a successful connection, or no verdict after a retry, fails the job).
6599# -SkipHttpErrorCheck is PowerShell 7+ only, so HTTP error statuses are caught below instead:
66100# any response at all means the TLS handshake (and thus the certificate chain) succeeded.
67101$progTmp = $ProgressPreference
68102$ProgressPreference = ' SilentlyContinue'
69103try {
70104 foreach ($endpoint in $TestEndpoints ) {
71105 $checkName = " Invoke-WebRequest rejects `` $endpoint `` "
72- try {
73- $res = Invoke-WebRequest - Uri $endpoint - UseBasicParsing - TimeoutSec 15
74- Record- Check $checkName ' FAIL' " Connection succeeded with HTTP $ ( $res.StatusCode ) (certificate was trusted)"
75- $GatingFailures ++
76- } catch {
77- if ($null -ne $_.Exception.Response ) {
78- Record- Check $checkName ' FAIL' " Connection succeeded with HTTP $ ( [int ]$_.Exception.Response.StatusCode ) (certificate was trusted)"
106+ # Retry non-certificate/network errors once; if the second attempt also fails,
107+ # the probe could not show that the certificate is rejected, so the check fails.
108+ for ($attempt = 1 ; $attempt -le 2 ; $attempt ++ ) {
109+ try {
110+ $res = Invoke-WebRequest - Uri $endpoint - UseBasicParsing - TimeoutSec 15
111+ Record- Check $checkName ' FAIL' " Connection succeeded with HTTP $ ( $res.StatusCode ) (certificate was trusted)"
79112 $GatingFailures ++
80- continue
81- }
82- $certRejected = $false
83- $hostnameMismatchOnly = $false
84- $ex = $_.Exception
85- while ($null -ne $ex ) {
86- if (
87- ($ex -is [System.Security.Authentication.AuthenticationException ] -and $ex.Message -match ' certificate chain|UntrustedRoot|RemoteCertificateChainErrors' ) -or
88- ($ex -is [System.Net.WebException ] -and $ex.Status -eq [System.Net.WebExceptionStatus ]::TrustFailure)
89- ) {
90- $certRejected = $true
113+ break
114+ } catch {
115+ if ($null -ne $_.Exception.Response ) {
116+ Record- Check $checkName ' FAIL' " Connection succeeded with HTTP $ ( [int ]$_.Exception.Response.StatusCode ) (certificate was trusted)"
117+ $GatingFailures ++
91118 break
92119 }
93- if ($ex -is [System.Security.Authentication.AuthenticationException ] -and $ex.Message -match ' RemoteCertificateNameMismatch' ) {
94- $hostnameMismatchOnly = $true
120+ $certRejected = $false
121+ $hostnameMismatchOnly = $false
122+ $ex = $_.Exception
123+ while ($null -ne $ex ) {
124+ if (
125+ ($ex -is [System.Security.Authentication.AuthenticationException ] -and $ex.Message -match ' certificate chain|UntrustedRoot|RemoteCertificateChainErrors' ) -or
126+ ($ex -is [System.Net.WebException ] -and $ex.Status -eq [System.Net.WebExceptionStatus ]::TrustFailure)
127+ ) {
128+ $certRejected = $true
129+ break
130+ }
131+ if ($ex -is [System.Security.Authentication.AuthenticationException ] -and $ex.Message -match ' RemoteCertificateNameMismatch' ) {
132+ $hostnameMismatchOnly = $true
133+ }
134+ $ex = $ex.InnerException
135+ }
136+ $cleanMsg = ($_.Exception.Message -replace ' [\r\n]+' , ' ' -replace ' \|' , ' \|' )
137+ if ($certRejected ) {
138+ Record- Check $checkName ' PASS' " Rejected with certificate validation error ($cleanMsg )"
139+ break
140+ } elseif ($hostnameMismatchOnly ) {
141+ Record- Check $checkName ' FAIL' " Certificate chain was trusted; failed only hostname check ($cleanMsg )"
142+ $GatingFailures ++
143+ break
144+ } elseif ($attempt -lt 2 ) {
145+ Write-Output " [INFO] ${checkName} : attempt $attempt failed with a non-certificate or network error ($cleanMsg ); retrying"
146+ Start-Sleep - Seconds 5
147+ } else {
148+ Record- Check $checkName ' FAIL' " Could not determine whether the certificate is rejected after $attempt attempts: non-certificate or network error ($cleanMsg )"
149+ $GatingFailures ++
95150 }
96- $ex = $ex.InnerException
97- }
98- $cleanMsg = ($_.Exception.Message -replace ' [\r\n]+' , ' ' -replace ' \|' , ' \|' )
99- if ($certRejected ) {
100- Record- Check $checkName ' PASS' " Rejected with certificate validation error ($cleanMsg )"
101- } elseif ($hostnameMismatchOnly ) {
102- Record- Check $checkName ' FAIL' " Certificate chain was trusted; failed only hostname check ($cleanMsg )"
103- $GatingFailures ++
104- } else {
105- Record- Check $checkName ' INCONCLUSIVE' " Non-certificate or network error ($cleanMsg )"
106151 }
107152 }
108153 }
0 commit comments