Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for TLS and connectionless LDAP connections on Linux #52904

Merged
merged 4 commits into from
Jun 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/libraries/Common/src/Interop/Interop.Ldap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ internal enum LdapOption
LDAP_OPT_SECURITY_CONTEXT = 0x99,
LDAP_OPT_ROOTDSE_CACHE = 0x9a, // Not Supported in Linux
LDAP_OPT_DEBUG_LEVEL = 0x5001,
LDAP_OPT_URI = 0x5006, // Not Supported in Windows
LDAP_OPT_X_SASL_REALM = 0x6101,
LDAP_OPT_X_SASL_AUTHCID = 0x6102,
LDAP_OPT_X_SASL_AUTHZID = 0x6103
Expand Down
19 changes: 9 additions & 10 deletions src/libraries/Common/src/Interop/Linux/OpenLdap/Interop.Ldap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ internal enum SaslChallengeType

internal static partial class Interop
{
public const string LDAP_SASL_SIMPLE = null;

internal static partial class Ldap
{
static Ldap()
Expand All @@ -75,10 +77,7 @@ static Ldap()
}

[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_initialize", CharSet = CharSet.Ansi, SetLastError = true)]
public static extern int ldap_initialize(out IntPtr ld, string hostname);

[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_init", CharSet = CharSet.Ansi, SetLastError = true)]
public static extern IntPtr ldap_init(string hostName, int portNumber);
public static extern int ldap_initialize(out IntPtr ld, string uri);

[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_unbind_ext_s", CharSet = CharSet.Ansi)]
public static extern int ldap_unbind_ext_s(IntPtr ld, ref IntPtr serverctrls, ref IntPtr clientctrls);
Expand Down Expand Up @@ -125,6 +124,9 @@ static Ldap()
[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_set_option", CharSet = CharSet.Ansi)]
public static extern int ldap_set_option_ptr([In] ConnectionHandle ldapHandle, [In] LdapOption option, ref IntPtr inValue);

[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_set_option", CharSet = CharSet.Ansi)]
public static extern int ldap_set_option_string([In] ConnectionHandle ldapHandle, [In] LdapOption option, string inValue);

[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_set_option", CharSet = CharSet.Ansi)]
public static extern int ldap_set_option_referral([In] ConnectionHandle ldapHandle, [In] LdapOption option, ref LdapReferralCallback outValue);

Expand All @@ -143,15 +145,12 @@ static Ldap()
[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_parse_reference", CharSet = CharSet.Ansi)]
public static extern int ldap_parse_reference([In] ConnectionHandle ldapHandle, [In] IntPtr result, ref IntPtr referrals, IntPtr ServerControls, byte freeIt);

[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_sasl_bind_s", CharSet = CharSet.Ansi)]
internal static extern int ldap_sasl_bind([In] ConnectionHandle ld, string dn, string mechanism, berval cred, IntPtr serverctrls, IntPtr clientctrls, IntPtr servercredp);

[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_sasl_interactive_bind_s", CharSet = CharSet.Ansi)]
internal static extern int ldap_sasl_interactive_bind([In] ConnectionHandle ld, string dn, string mechanism, IntPtr serverctrls, IntPtr clientctrls, uint flags, [MarshalAs(UnmanagedType.FunctionPtr)] LDAP_SASL_INTERACT_PROC proc, IntPtr defaults);

[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_simple_bind_s", CharSet = CharSet.Ansi, SetLastError = true)]
public static extern int ldap_simple_bind([In] ConnectionHandle ld, string who, string passwd);

[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_bind_s", CharSet = CharSet.Ansi, SetLastError = true)]
public static extern int ldap_bind_s([In] ConnectionHandle ld, string who, string passwd, int method);

[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_err2string", CharSet = CharSet.Ansi)]
public static extern IntPtr ldap_err2string(int err);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,34 @@ and to test and view status

docker exec -it slapd01 slapcat

SLAPD OPENLDAP SERVER WITH TLS
==============================

The osixia/openldap container image automatically creates a TLS lisener with a self-signed certificate. This can be used to test TLS.

Start the container, with TLS on port 1636, without client certificate verification:

docker run --publish 1389:389 --publish 1636:636 --name ldap --hostname ldap.local --detach --rm --env LDAP_TLS_VERIFY_CLIENT=never --env LDAP_ADMIN_PASSWORD=password osixia/openldap --loglevel debug

Extract the CA certificate and write to a temporary file:

docker exec ldap cat /container/service/slapd/assets/certs/ca.crt > /tmp/ca.crt

Set the LDAP client CA certificate path in `/etc/ldap/ldap.conf` so OpenLDAP trusts the self-signed certificate:

# /etc/ldap/ldap.conf
#...
TLS_CACERT /tmp/ca.crt

Finally, map the `ldap.local` hostname manually set above to the loopback address:

# /etc/hosts
127.0.0.1 ldap.local

To test and view the status:

ldapsearch -H ldaps://ldap.local:1636 -b dc=example,dc=org -x -D cn=admin,dc=example,dc=org -w password

ACTIVE DIRECTORY
================

Expand Down Expand Up @@ -83,5 +111,14 @@ Note:
<Password>%TESTPASSWORD%</Password>
<AuthenticationTypes>ServerBind,None</AuthenticationTypes>
</Connection>
<Connection Name="SLAPD OPENLDAP SERVER TLS">
iinuwa marked this conversation as resolved.
Show resolved Hide resolved
<ServerName>ldap.local</ServerName>
<SearchDN>DC=example,DC=org</SearchDN>
<Port>1636</Port>
<User>cn=admin,dc=example,dc=org</User>
<Password>password</Password>
<AuthenticationTypes>ServerBind,None</AuthenticationTypes>
<UseTls>true</UseTls>
</Connection>

</Configuration>
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ namespace System.DirectoryServices.Tests
{
internal class LdapConfiguration
{
private LdapConfiguration(string serverName, string searchDn, string userName, string password, string port, AuthenticationTypes at)
private LdapConfiguration(string serverName, string searchDn, string userName, string password, string port, AuthenticationTypes at, bool useTls)
{
ServerName = serverName;
SearchDn = searchDn;
UserName = userName;
Password = password;
Port = port;
AuthenticationTypes = at;
UseTls = useTls;
}

private static LdapConfiguration s_ldapConfiguration = GetConfiguration("LDAP.Configuration.xml");
Expand All @@ -30,6 +31,7 @@ private LdapConfiguration(string serverName, string searchDn, string userName, s
internal string Port { get; set; }
internal string SearchDn { get; set; }
internal AuthenticationTypes AuthenticationTypes { get; set; }
internal bool UseTls { get; set; }
internal string LdapPath => string.IsNullOrEmpty(Port) ? $"LDAP://{ServerName}/{SearchDn}" : $"LDAP://{ServerName}:{Port}/{SearchDn}";
internal string RootDSEPath => string.IsNullOrEmpty(Port) ? $"LDAP://{ServerName}/rootDSE" : $"LDAP://{ServerName}:{Port}/rootDSE";
internal string UserNameWithNoDomain
Expand Down Expand Up @@ -104,6 +106,7 @@ internal static LdapConfiguration GetConfiguration(string configFile)
string user = "";
string password = "";
AuthenticationTypes at = AuthenticationTypes.None;
bool useTls = false;

XElement child = connection.Element("ServerName");
if (child != null)
Expand Down Expand Up @@ -132,6 +135,12 @@ internal static LdapConfiguration GetConfiguration(string configFile)
password = val;
}

child = connection.Element("UseTls");
if (child != null)
{
useTls = bool.Parse(child.Value);
}

child = connection.Element("AuthenticationTypes");
if (child != null)
{
Expand Down Expand Up @@ -161,7 +170,7 @@ internal static LdapConfiguration GetConfiguration(string configFile)
at |= AuthenticationTypes.Signing;
}

ldapConfig = new LdapConfiguration(serverName, searchDn, user, password, port, at);
ldapConfig = new LdapConfiguration(serverName, searchDn, user, password, port, at, useTls);
}
}
catch (Exception ex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,32 @@ internal static int SearchDirectory(ConnectionHandle ldapHandle, string dn, int

internal static int SetPtrOption(ConnectionHandle ldapHandle, LdapOption option, ref IntPtr inValue) => Interop.Ldap.ldap_set_option_ptr(ldapHandle, option, ref inValue);

internal static int SetStringOption(ConnectionHandle ldapHandle, LdapOption option, string inValue) => Interop.Ldap.ldap_set_option_string(ldapHandle, option, inValue);

internal static int SetReferralOption(ConnectionHandle ldapHandle, LdapOption option, ref LdapReferralCallback outValue) => Interop.Ldap.ldap_set_option_referral(ldapHandle, option, ref outValue);

// This option is not supported in Linux, so it would most likely throw.
internal static int SetServerCertOption(ConnectionHandle ldapHandle, LdapOption option, VERIFYSERVERCERT outValue) => Interop.Ldap.ldap_set_option_servercert(ldapHandle, option, outValue);

internal static int BindToDirectory(ConnectionHandle ld, string who, string passwd) => Interop.Ldap.ldap_simple_bind(ld, who, passwd);
internal static int BindToDirectory(ConnectionHandle ld, string who, string passwd)
{
IntPtr passwordPtr = IntPtr.Zero;
try
{
passwordPtr = LdapPal.StringToPtr(passwd);
berval passwordBerval = new berval
{
bv_len = passwd.Length,
bv_val = passwordPtr,
};

return Interop.Ldap.ldap_sasl_bind(ld, who, Interop.LDAP_SASL_SIMPLE, passwordBerval, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
}
finally
{
Marshal.FreeHGlobal(passwordPtr);
joperezr marked this conversation as resolved.
Show resolved Hide resolved
}
}

internal static int StartTls(ConnectionHandle ldapHandle, ref int ServerReturnValue, ref IntPtr Message, IntPtr ServerControls, IntPtr ClientControls) => Interop.Ldap.ldap_start_tls(ldapHandle, ref ServerReturnValue, ref Message, ServerControls, ClientControls);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics;
using System.Net;
using System.Text;
using System.Runtime.InteropServices;

namespace System.DirectoryServices.Protocols
Expand All @@ -12,13 +13,67 @@ public partial class LdapConnection
// Linux doesn't support setting FQDN so we mark the flag as if it is already set so we don't make a call to set it again.
private bool _setFQDNDone = true;

private void InternalInitConnectionHandle(string hostname) => _ldapHandle = new ConnectionHandle(Interop.Ldap.ldap_init(hostname, ((LdapDirectoryIdentifier)_directoryIdentifier).PortNumber), _needDispose);
private void InternalInitConnectionHandle(string hostname)
{
if ((LdapDirectoryIdentifier)_directoryIdentifier == null)
{
throw new NullReferenceException();
}

_ldapHandle = new ConnectionHandle();
}

private int InternalConnectToServer()
{
// In Linux you don't have to call Connect after calling init. You
// directly call bind. However, we set the URI for the connection
// here instead of during initialization because we need access to
// the SessionOptions property to properly define it, which is not
// available during init.
Debug.Assert(!_ldapHandle.IsInvalid);
// In Linux you don't have to call Connect after calling init. You directly call bind.
return 0;

string scheme = null;
LdapDirectoryIdentifier directoryIdentifier = (LdapDirectoryIdentifier)_directoryIdentifier;
if (directoryIdentifier.Connectionless)
{
scheme = "cldap://";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have any test for connectionless?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I wasn't sure how to test it. I think that since connectionless LDAP isn't standardized, it's only implemented in AD (or as far as I can tell, it's not implemented in slapd). I've only got access to one [production] instance of AD, so I didn't want to test there! :)

I imagine that this is currently broken in Linux since there are no provisions anywhere else to configure connectionless. I think that this patch should work for connectionless should work though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally we don't add features we can't test 🙂. But if the feature is simply "given this flag, use this scheme prefix" maybe it's reasonable to add if testing isn't straightforward.

@joperezr preferences?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that having a test would be ideally, but given the context here I think that if we can't come up with a good way to test this it may be reasonable.

}
else if (SessionOptions.SecureSocketLayer)
{
scheme = "ldaps://";
}
else
{
scheme = "ldap://";
}

string uris = null;
string[] servers = directoryIdentifier.Servers;
if (servers != null && servers.Length != 0)
{
StringBuilder temp = new StringBuilder(200);
for (int i = 0; i < servers.Length; i++)
{
if (i != 0)
{
temp.Append(' ');
}
temp.Append(scheme);
temp.Append(servers[i]);
temp.Append(':');
temp.Append(directoryIdentifier.PortNumber);
}
if (temp.Length != 0)
{
uris = temp.ToString();
}
}
else
{
uris = $"{scheme}:{directoryIdentifier.PortNumber}";
}

return LdapPal.SetStringOption(_ldapHandle, LdapOption.LDAP_OPT_URI, uris);
Copy link
Member

@danmoseley danmoseley May 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible for directoryIdentifier.Servers to, through bad input, contains only servers that are empty strings? If so uris will be null but it appears openldap treats that as return to default. Not sure whether that's what you want, or you want to avoid calling it in that case.
https://git.openldap.org/openldap/openldap/-/blob/master/libraries/libldap/options.c#L655

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is possible. As you said, OpenLDAP falls back to its default hostname (which can be configured in /etc/ldap/ldap.conf) if the hostname is not set. I intentionally allowed that behavior here too since it would be expected if someone is familiar with OpenLDAP.

I think that's OK... though I don't know how the Windows side handles empty string hostnames. If someone wrote code on Linux, then tried to run on Windows, that could be an issue if Windows doesn't support default hostnames.

}

private int InternalBind(NetworkCredential tempCredential, SEC_WINNT_AUTH_IDENTITY_EX cred, BindMethod method)
Expand All @@ -30,7 +85,7 @@ private int InternalBind(NetworkCredential tempCredential, SEC_WINNT_AUTH_IDENTI
}
else
{
error = Interop.Ldap.ldap_simple_bind(_ldapHandle, cred.user, cred.password);
error = LdapPal.BindToDirectory(_ldapHandle, cred.user, cred.password);
}

return error;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ public partial class LdapSessionOptions
{
private static void PALCertFreeCRLContext(IntPtr certPtr) { /* No op */ }

[SupportedOSPlatform("windows")]
danmoseley marked this conversation as resolved.
Show resolved Hide resolved
public bool SecureSocketLayer
public bool SecureSocketLayer { get; set; }

public int ProtocolVersion
{
get => throw new PlatformNotSupportedException();
set => throw new PlatformNotSupportedException();
get => GetPtrValueHelper(LdapOption.LDAP_OPT_VERSION).ToInt32();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes a problem with setting the LDAP version on the connection by using IntPtr types rather than int directly.

Could you say more?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my testing, I had a problem where the version was not being set properly when using an integer rather than an IntPtr, so it was defaulting to LDAPv2 on my machine and failing to connect. The API calls for an pointer to an int:

LDAP_OPT_PROTOCOL_VERSION
Sets/gets the protocol version. outvalue and invalue must be
int *.

I think this might work now because the value is being cast to an integer pointer on the C side, but from what I understand (which is very little) I think that this can fail on systems with different pointer sizes?

set => SetPtrValueHelper(LdapOption.LDAP_OPT_VERSION, new IntPtr(value));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,11 @@ public bool SecureSocketLayer
SetIntValueHelper(LdapOption.LDAP_OPT_SSL, temp);
}
}

public int ProtocolVersion
{
get => GetIntValueHelper(LdapOption.LDAP_OPT_VERSION);
set => SetIntValueHelper(LdapOption.LDAP_OPT_VERSION, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,6 @@ public int ReferralHopLimit
}
}

public int ProtocolVersion
{
get => GetIntValueHelper(LdapOption.LDAP_OPT_VERSION);
set => SetIntValueHelper(LdapOption.LDAP_OPT_VERSION, value);
}

public string HostName
{
get => GetStringValueHelper(LdapOption.LDAP_OPT_HOST_NAME, false);
Expand Down Expand Up @@ -787,6 +781,33 @@ private void SetIntValueHelper(LdapOption option, int value)
ErrorChecking.CheckAndSetLdapError(error);
}

private IntPtr GetPtrValueHelper(LdapOption option)
{
if (_connection._disposed)
{
throw new ObjectDisposedException(GetType().Name);
}

IntPtr outValue = new IntPtr(0);
int error = LdapPal.GetPtrOption(_connection._ldapHandle, option, ref outValue);
ErrorChecking.CheckAndSetLdapError(error);

return outValue;
}

private void SetPtrValueHelper(LdapOption option, IntPtr value)
{
if (_connection._disposed)
{
throw new ObjectDisposedException(GetType().Name);
}

IntPtr temp = value;
int error = LdapPal.SetPtrOption(_connection._ldapHandle, option, ref temp);

ErrorChecking.CheckAndSetLdapError(error);
}

private string GetStringValueHelper(LdapOption option, bool releasePtr)
{
if (_connection._disposed)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ private LdapConnection GetConnection()
// Set server protocol before bind; OpenLDAP servers default
// to LDAP v2, which we do not support, and will return LDAP_PROTOCOL_ERROR
connection.SessionOptions.ProtocolVersion = 3;
connection.SessionOptions.SecureSocketLayer = LdapConfiguration.Configuration.UseTls;
danmoseley marked this conversation as resolved.
Show resolved Hide resolved
connection.Bind();

connection.Timeout = new TimeSpan(0, 3, 0);
Expand Down