Capture and Examine Server Certificate from AD Connections

Have you ever deployed an app into a Windows domain that uses Active Directory authentication, and sometimes it works and other times it doesn’t? This can be an especially annoying issue if you aren’t a domain admin and you can’t log into your domain controllers to examine their settings.

Fortunately, if you suspect that the problem may be the SSL Certificate the server is using when you connect to it there is something you can to do conclusively troubleshoot the issue. First let’s talk about the issue I ran into.

When your app wants to talk to Active Directory to validate credentials it can do so without using SSL if it connects to port 389. If your app is using that port then read on for curiosity’s sake only because this isn’t your problem. If your connection string to AD specifies port 636, you are using SSL. This will only work if the server you are connecting to hands you a certificate in the connection attempt that your machine trusts.

In a normal domain setup this isn’t a problem. The AD server should have a certificate issued by either a domain CA that all machines trust, or a third party cert that again all machines trust, this time because the CA is recognized by Windows automatically. These are certs issued by Verisign or something like that.

If the certificate you receive from the AD server is not trusted, your connection will fail, authentication will fail, and the smoking gun will be a very easy to read Windows Event Log entry like the one below.

CertErrorLogCrop

That error log entry tells you very specifically what the issue was, but it leaves out some important information.

  • What server did I connect to that failed?
    • Connection strings often just specify the domain name not the specific server to connect to. If you have multiple domain controllers you could end up connecting to any of them for any given connection attempt.
  • What certificate did it give me that just failed?
    • Admins hate it when you just point fingers and say “your thing is broken” with no other details. It would be nice to be able to say to your domain admins “hey, I got this certificate from this issuer and my machine doesn’t trust it. Let’s work this out.”

To get that information, and to get to the point of this article, we are going to do a couple things. We are going to find all of the domain controllers on the network, and then connect to them one by one and grab the certificate out of the connection to examine it.

To find the domain controllers we can ask directory services to just give us the list.

$controllers = [directoryServices.ActiveDirectory.Domain]::GetComputerDomain().FindAllDomainControllers()

In a moment we’ll iterate over that list to get the certs, but first we need the function to get it.

function Get-ADCert {
    param(
        [string]$server
    )

    [system.reflection.assembly]::loadwithpartialname('System.DirectoryServices.Protocols') | Out-NULL

    # This script block is run instead of the normal system cert validation code.    
    $DelegateScriptBlock = {
        PARAM(
            [System.DirectoryServices.Protocols.LdapConnection] $LDAPConnection,
            [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate
        )
    
        PROCESS{
            if ($Certificate -eq $null){
                Write-Verbose "Error - No Certificate"
                return $false
            }
    
            $collection = New-Object System.Security.Cryptography.X509Certificates.X509Chain
            $collection.Build($Certificate)
            # populate variable with script scope to make it accessible outside this code block
            $Script:adCert = $collection
    
            return $true
        }
    }
    
    
    $connection=new-object System.DirectoryServices.Protocols.LDAPConnection("$server`:636")
    $options=$connection.SessionOptions;
    $options.ProtocolVersion=3
    $options.SecureSocketLayer=$true
    
    # This property allows us to substitute the normal cert validation for our own code block.
    $options.VerifyServerCertificate = $DelegateScriptBlock
    $connection.AuthType='Basic'
    try{
        # Call bind to establish the connection. Our validation code is run during execution of bind(). We then throw the connection away.
        $connection.Bind()
        $connection.Dispose()
    }
    catch{ 
        "`r`nBind failed for controller: $controller - `r`n With Error: $_"
    }

    if($adCert){
        # passing the cert as output is just an easy way to populate a variable with the result of this function.
        Write-Output $adCert
    } else {
        Write-Error "Certificate Not Found for Controller: $server"
    }

}

Now we can loop over the domain controllers and get the certs.

$certs = @{}

foreach($dc in $controllers)
{
    $certs."$($dc.name)" = Get-ADCert -server $dc.name
}

Each index of the $certs hash table is now the server certificate along with each certificate in the certification path. To examine the certs we can issue a command like below.

$certs.Values | ForEach-Object {$_.chainelements[0].certificate} | Format-Table issuer,subject,@{L='SelfSigned';E={$_.issuer -eq $_.subject}} -AutoSize

That command would show you very clearly if a domain controller is trying to use a self signed certificate, and yeah, that happens to me a lot. You can also look at the issuer and see if you trust that issuer in your local machines Root certificate store.

Thanks and I hope you find this useful.

Advertisements