I’ve recently bumped into a need to verify certificates deployed for LDAPS on Active Directory. PowerShell remains my tool of choice for such occasions as this.
It is possible to very simply test an LDAPS connection using ADSI (System.DirectoryServices) like this:
$DirectoryEntry = New-Object DirectoryServices.DirectoryEntry("LDAP://someserver.domain.example") $DirectoryEntry.AuthenticationType = [DirectoryServices.AuthenticationTypes]"Secure, SecureSocketsLayer, ServerBind" $DirectoryEntry
A big red error suggests LDAPS is not avaiable of there is some other problem with the LDAP service. Anything else means the connection must have succeeded. We can infer that SSL negotiation also succeeded since we asked for it. Note that Secure is used to bind using the current users credentials, it is not necessary to provide explicit credentials for this. ServerBind removes any chance of the DCLocator becoming involved.
This method doesn’t really reveal very much unless you run a packet sniffer in parallel. I’m much happier when I can dig a bit deeper.
Happily the System.DirectoryServices.Protocols assembly contains just the tools we need to dig. It contains:
- A means of defining an LDAP connection with a reasonable amount of detail
- A way of showing negotiated SSL values such as protocols, hashing algorithms, and so on.
- A callback delegate which will let us take a look at the certificate used for the connection.
Before we start, System.DirectoryServices.Protocols must be imported into the current session.
Add-Type -Assembly System.DirectoryServices.Protocols
Constructing the LDAP connection is simple. I elected to use the LdapDirectoryIdentifier, it saves a bit of messing around constructing strings describing the end-point.
$ComputerName = "someserver.domain.example" $Port = 636 $DirectoryIdentifier = New-Object DirectoryServices.Protocols.LdapDirectoryIdentifier($ComputerName, $Port)
Next task is to build the LdapConnection object. Using Kerberos as the AuthType allows us to continue without authenticating again.
$Connection = New-Object DirectoryServices.Protocols.LdapConnection($DirectoryIdentifier) $Connection.AuthType = [DirectoryServices.Protocols.AuthType]::Kerberos
Next a few session options should be set. By default the connection will be plain text and LDAPv2 will be used.
$Connection.SessionOptions.ProtocolVersion = 3 $Connection.SessionOptions.SecureSocketLayer = $true
If you look at the Connection object you should see an SslInformation property. Once we’ve bound to the directory this will be filled with algorithm information assuming SSL is successfully negotiated.
Getting any information about the certificate back needs a Callback Delegate defining for the VerifyServerCertificate property. Happily doing this needs nothing more complex than a ScriptBlock. I’m going to use a global scoped variable to let that return something to the main code block.
New-Variable LdapCertificate -Scope Global -Force $Connection.SessionOptions.VerifyServerCertificate = { param( [DirectoryServices.Protocols.LdapConnection]$Connection, [Security.Cryptography.X509Certificates.X509Certificate2]$Certificate ) $Global:LdapCertificate = $Certificate return $true }
If you follow the link you’ll find the second argument passed into the delegate is X509Certificate (not X509Certificate2). Happily we can cast from one to the other, this exposes a few more properties we can immediately use.
Having assembled all of that, asking the connection to Bind will connect to the directory service and, hopefully, show us the certificate used.
$Connection.Bind()
As with the simple version at the top, a big red error suggests the directory service either doesn’t have a certificate or something else went wrong.
Having bound you can inspect these two to see what happened (note that LdapCertificate is the global variable):
$Connection.SessionOptions.SslInformation $LdapCertificate | Format-List *
Finally, I’ve taken all that and wrapped it into a function which will return that as a handy object.
function Test-InLdapSSLConnection { # .SYNOPSIS # Test and LDAPS connection. # .DESCRIPTION # Test an LDAP connection returning information about the negotiated SSL connection including the server certificate. # # The state message "The LDAP server is unavailable" indicates the server is either offline or unwilling to negotiate an SSL connection. # .PARAMETER ComputerName # The name of a computer to test. By default serverless binding is used. # .PARAMETER Credential # Credentials to use for the bind attempt. This command requires no special privileges. # .PARAMETER Port # The port to connect to, by default the LDAPS port (636) is used. # .INPUTS # System.Management.Automation.PSCredential # System.String # System.UInt16 # .OUTPUTS # Indented.LDAP.ConnectionInformation # .EXAMPLE # Test-InLdapSSLConnection # # Attempt to bind using SSL and serverless binding. # .EXAMPLE # Test-InLdapSSLConnection -ComputerName servername # # Attempt to negotiate SSL with "servername". # .NOTES # Author: Chris Dent # # Change log: # 31/03/2015 - Chris Dent - First release. param( [Parameter(ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)] [Alias('DnsHostName')] [String]$ComputerName = "", [UInt16]$Port = 636, [PSCredential]$Credential ) process { $DirectoryIdentifier = New-Object DirectoryServices.Protocols.LdapDirectoryIdentifier($ComputerName, $Port) if ($psboundparameters.ContainsKey("Credential")) { $Connection = New-Object DirectoryServices.Protocols.LdapConnection($DirectoryIdentifier, $Credential.GetNetworkCredential()) $Connection.AuthType = [DirectoryServices.Protocols.AuthType]::Basic } else { $Connection = New-Object DirectoryServices.Protocols.LdapConnection($DirectoryIdentifier) $Connection.AuthType = [DirectoryServices.Protocols.AuthType]::Kerberos } $Connection.SessionOptions.ProtocolVersion = 3 $Connection.SessionOptions.SecureSocketLayer = $true # Declare a script level variable which can be used to return information from the delegate. New-Variable LdapCertificate -Scope Script -Force # Create a callback delegate to retrieve the negotiated certificate. # Note: # * The certificate is unlikely to return the subject. # * The delegate is documented as using the X509Certificate type, automatically casting this to X509Certificate2 allows access to more information. $Connection.SessionOptions.VerifyServerCertificate = { param( [DirectoryServices.Protocols.LdapConnection]$Connection, [Security.Cryptography.X509Certificates.X509Certificate2]$Certificate ) $Script:LdapCertificate = $Certificate return $true } $State = "Connected" try { $Connection.Bind() } catch { $State = "Failed ($($_.Exception.InnerException.Message.Trim()))" } $ConnectionInformation = New-Object PSObject -Property ([Ordered]@{ ComputerName = $ComputerName Port = $Port State = $State Protocol = $Connection.SessionOptions.SslInformation.Protocol AlgorithmIdentifier = $Connection.SessionOptions.SslInformation.AlgorithmIdentifier CipherStrength = $Connection.SessionOptions.SslInformation.CipherStrength Hash = $Connection.SessionOptions.SslInformation.Hash HashStrength = $Connection.SessionOptions.SslInformation.HashStrength KeyExchangeAlgorithm = [Security.Authentication.ExchangeAlgorithmType][Int]$Connection.SessionOptions.SslInformation.KeyExchangeAlgorithm ExchangeStrength = $Connection.SessionOptions.SslInformation.ExchangeStrength X509Certificate = $Script:LdapCertificate }) $ConnectionInformation.PSObject.TypeNames.Add("Indented.LDAP.ConnectionInformation") return $ConnectionInformation } }