When you deploy certificates you need to ensure you trust the "chain" of the certificate authority this will usually look like this:
Certificate > Intermediate Certificate Authority > Root Certificate Authority
It is best practice for in this example, where we are using internal Certificate Authority servers that all the chain is trusted on devices that cannot receive the certificates, this should be outlined and provided in the certificate being used, but you should not be trusting this path:
Certificate > Root Certificate Authority
If this is the approach then you will get trust issues the certificate at the section is red below as it is not trusted implicitly:
Certificate > Intermediate Certificate Authority > Root Certificate Authority
If this rule is not enforced on the client then this error will be visible in the browser, in this case Chrome:
This means we are aware of the Root Certificate Authority but not the Intermediate Authority meaning that the chain is broken and that will give you the problem shown above.
I therefore created a script to check for this and this will check that the certificate is valid and that the remote server is presenting the certificates correctly to the client as well, this is an example of this:
In the above example you can see that the we have 3 x certificates which are all correctly being presented by the server and not relying to the local store, this means this certificate is healthy and client that visit this site will not get the error outlined above.
Script : Check-CertChain.ps1
# PowerShell Certificate Chain Checker with Chain Presentation Verification
# This script verifies both the chain validity and how certificates are obtained
function Test-CertificateChainPS {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$FQDN
)
Write-Host "`nChecking certificate chain for: $FQDN" -ForegroundColor Cyan
try {
Write-Verbose "Creating TCP Client..."
$port = 443
$tcpClient = New-Object System.Net.Sockets.TcpClient
Write-Host "Connecting to $FQDN..." -ForegroundColor Yellow
$tcpClient.Connect($FQDN, $port)
if (-not $tcpClient.Connected) {
Write-Host "Failed to connect to $FQDN on port $port" -ForegroundColor Red
return
}
Write-Verbose "Creating SSL Stream..."
# Create callback to capture server-provided certificates
$serverCertificates = New-Object System.Collections.Generic.List[System.Security.Cryptography.X509Certificates.X509Certificate2]
$remoteCertificateCallback = {
param($sender, $certificate, $chain, $errors)
# Capture each certificate presented by the server
if ($chain) {
foreach ($element in $chain.ChainElements) {
$serverCertificates.Add($element.Certificate)
}
}
return $true
}
$sslStream = New-Object System.Net.Security.SslStream(
$tcpClient.GetStream(),
$false,
$remoteCertificateCallback
)
Write-Verbose "Authenticating as client..."
$sslStream.AuthenticateAsClient($FQDN)
Write-Host "-------------------------------------" -ForegroundColor Green
Write-Host "Certificates directly presented by server: $($serverCertificates.Count)" -ForegroundColor White
Write-Host "`nComplete Chain Analysis:" -ForegroundColor Green
Write-Host "----------------------" -ForegroundColor Green
$remoteCert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($sslStream.RemoteCertificate)
$chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain
# Configure chain building options
$chain.ChainPolicy.RevocationFlag = [System.Security.Cryptography.X509Certificates.X509RevocationFlag]::EntireChain
$chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::Online
$chain.ChainPolicy.UrlRetrievalTimeout = New-TimeSpan -Seconds 30
# Configure verification flags to allow unknown certificate authorities
$chain.ChainPolicy.VerificationFlags = [System.Security.Cryptography.X509Certificates.X509VerificationFlags]::AllowUnknownCertificateAuthority
Write-Verbose "Building complete certificate chain..."
$chainBuilt = $chain.Build($remoteCert)
# Track which certificates were downloaded vs presented
$downloadedCerts = New-Object System.Collections.Generic.List[string]
# Display each certificate in the chain
for ($i = 0; $i -lt $chain.ChainElements.Count; $i++) {
$cert = $chain.ChainElements[$i].Certificate
Write-Host "`nCertificate $($i + 1):" -ForegroundColor Yellow
Write-Host "Subject: $($cert.Subject)" -ForegroundColor White
Write-Host "Issuer: $($cert.Issuer)" -ForegroundColor White
Write-Host "Valid From: $($cert.NotBefore)" -ForegroundColor White
Write-Host "Valid To: $($cert.NotAfter)" -ForegroundColor White
# Determine certificate type and source
if ($i -eq 0) {
$certType = "End-Entity Certificate"
}
elseif ($i -eq $chain.ChainElements.Count - 1) {
$certType = "Root CA Certificate"
}
else {
$certType = "Intermediate Certificate"
}
# Check if this certificate was presented by server
$wasPresented = $false
foreach ($serverCert in $serverCertificates) {
if ($cert.Thumbprint -eq $serverCert.Thumbprint) {
$wasPresented = $true
break
}
}
Write-Host "Type: $certType" -ForegroundColor Cyan
if ($wasPresented) {
Write-Host "Source: Presented by Server" -ForegroundColor Green
}
else {
Write-Host "Source: Retrieved from Network/Local Store" -ForegroundColor Yellow
$downloadedCerts.Add($cert.Subject)
}
# Check for any status flags
if ($chain.ChainElements[$i].ChainElementStatus.Length -gt 0) {
Write-Host "Status Flags:" -ForegroundColor Yellow
foreach ($status in $chain.ChainElements[$i].ChainElementStatus) {
Write-Host "- $($status.StatusInformation)" -ForegroundColor Gray
}
}
}
# Display chain validation result and missing certificates
Write-Host "`nChain Analysis Summary:" -ForegroundColor Green
Write-Host "---------------------" -ForegroundColor Green
Write-Host "Total certificates in chain: $($chain.ChainElements.Count)" -ForegroundColor White
Write-Host "Certificates presented by server: $($serverCertificates.Count)" -ForegroundColor White
Write-Host "Certificates retrieved from other sources: $($downloadedCerts.Count)" -ForegroundColor White
if ($downloadedCerts.Count -gt 0) {
Write-Host "`nCertificates that had to be retrieved:" -ForegroundColor Yellow
foreach ($cert in $downloadedCerts) {
Write-Host "- $cert" -ForegroundColor Gray
}
}
if ($chainBuilt) {
Write-Host "`n✓ Certificate chain is valid" -ForegroundColor Green
if ($serverCertificates.Count -lt $chain.ChainElements.Count) {
Write-Host "! Warning: Not all certificates are being presented by the server" -ForegroundColor Yellow
Write-Host " This may cause issues with clients that cannot download missing certificates" -ForegroundColor Yellow
}
}
else {
Write-Host "`n✗ Certificate chain validation failed" -ForegroundColor Red
Write-Host "Chain Status Details:" -ForegroundColor Yellow
foreach ($element in $chain.ChainElements) {
foreach ($status in $element.ChainElementStatus) {
Write-Host "- $($status.StatusInformation)" -ForegroundColor Red
}
}
}
}
catch {
Write-Host "`nError: An unexpected error occurred" -ForegroundColor Red
Write-Host "Error details: $_" -ForegroundColor Red
Write-Host "Stack trace:" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor Gray
}
finally {
if ($sslStream) { $sslStream.Dispose() }
if ($tcpClient) { $tcpClient.Dispose() }
}
}
# Main script execution
Clear-Host
Write-Host "Certificate Chain Checker (Skeletor)" -ForegroundColor Cyan
Write-Host "-------------------------------------" -ForegroundColor Cyan
$FQDN = Read-Host "`nPlease enter the FQDN to check"
Test-CertificateChainPS -FQDN $FQDN -Verbose