3
votes

We have an application that authenticates against a remote AD using LDAP, by IP address, over a VPN tunnel, using the following code:

using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, ldap.Host, ldap.Path.Replace("/", ""), ContextOptions.Negotiate, UserName, Password))
{
    using (UserPrincipal user = UserPrincipal.FindByIdentity(pc, domainAndUsername))
    {
        if (user != null)
        {
            SAMAccountName = user.SamAccountName;
            retVal = true;
        }
    }
}

This works great for normal, non-SSL LDAP. However, we've encountered a situation where we need to be connecting to LDAPS, via SSL, and it doesn't work. I've tried a ton of variations on the PrincipalContext constructor, but everything we do results in a failed connection, with this error:

System.DirectoryServices.AccountManagement.PrincipalServerDownException: The server could not be contacted. ---> System.DirectoryServices.Protocols.LdapException: The LDAP server is unavailable.
   at System.DirectoryServices.Protocols.LdapConnection.Connect()
   at System.DirectoryServices.Protocols.LdapConnection.SendRequestHelper(DirectoryRequest request, Int32& messageID)
   at System.DirectoryServices.Protocols.LdapConnection.SendRequest(DirectoryRequest request, TimeSpan requestTimeout)
   at System.DirectoryServices.AccountManagement.PrincipalContext.ReadServerConfig(String serverName, ServerProperties& properties)
   --- End of inner exception stack trace ---
   at System.DirectoryServices.AccountManagement.PrincipalContext.ReadServerConfig(String serverName, ServerProperties& properties)
   at System.DirectoryServices.AccountManagement.PrincipalContext.DoServerVerifyAndPropRetrieval()
   at System.DirectoryServices.AccountManagement.PrincipalContext..ctor(ContextType contextType, String name, String container, ContextOptions options, String userName, String password)

We know it's not the LDAP server itself, as trying by the method discussed here connects without errors. I'm not really comfortable with using try...catch for logic flow, and I've read of some other issues with this method (like not properly respecting certificates and such), so I'm trying to make this work with PrincipalContext.

Could anybody offer me some guidance here? I'm going crazy on this one.

EDIT: Upon some further research, it looks like this may be a problem with self-signed certificates.

EDIT 2: After breaking it down into X509 chain calls, I'm getting this specific error:

PartialChain A certificate chain could not be built to a trusted root authority.

Looking at that, you'd think it's just a matter of adding the CA as a trusted root, but that didn't seem to fix the problem.

2
Are you using port 636? - scotru
@scotru Yes, we are using port 636. The whole problem here is that the certificate validation always fails. - BRW
Is the cert installed on the computer you are testing from if self-signed? I know with LdapConnection you can implement your own certificate validation. Not sure if something like that is possible with PrincipalContext. Maybe try LdapConnection? - scotru
@scotru Yeah, we installed the cert locally. I ended up breaking it down into a bunch of X509 chain calls and found specific errors. See edit for details. - BRW

2 Answers

3
votes

I'm wondering if this CodeProject article may be of assistance:

"Recently, I ran into trouble using System.DirectoryServices.DirectoryEntry to connect to a Novell eDirectory server because the certificate was self-signed. When run in an ASP.NET application, the machine-level certificate store was not examined. So, even though the self-signed certificate was in the trusted store, DirectoryEntry was still refusing to establish a connection.

The LdapConnection class is a better choice for this situation, as it allows the user to validate the certificate manually. Note that the DirectoryEntry approach works fine with a trusted self-signed certificate when run in a Windows Forms application."

With LdapConnection, you could use your own certificate authentication as shown here:

LdapConnection con = new LdapConnection(new LdapDirectoryIdentifier("EDIRECTORYSERVER:636"));
con.SessionOptions.SecureSocketLayer = true;
con.SessionOptions.VerifyServerCertificate = 
   new VerifyServerCertificateCallback(ServerCallback);
con.Credential = new NetworkCredential(String.Empty, String.Empty);
con.AuthType = AuthType.Basic;

And a verification callback like this:

public static bool ServerCallback(LdapConnection connection, X509Certificate certificate)
{
  try
  {
    X509Certificate expectedCert = 
         X509Certificate.CreateFromCertFile("C:\\certificates\\certificate.cer");

    if (expectedCert.Equals(certificate))
    {
        return true;
    }
    else
    {
        return false;
    }
  }
  catch (Exception ex)
  {
      return false;
  }
}
0
votes

Check this answer by Gabriel Luci

Are you sure it supports SSL and that the firewall is open to allow that connection?

LDAP uses port 389. LDAPS uses port 636.

If you have the telnet client installed, you can use it to check the connectivity:

telnet yourdomain.com 636 If you get a blank screen, it worked. If it can't connect, it will tell you.

If that is open and it still does not work, it could be using a self-signed SSL certificate. Check the Windows event log for certificate-related errors.

I've also used Chrome to check the certificate. You have to run chrome like this:

chrome.exe --explicitly-allowed-ports=636 Then browse to https://yourdomain.com:636 and see if it gives you any certificate errors. Then you can actually see the certificate. If that's the problem, you may be able to import the certificate and explicitly trust it.