Skip to content

Commit

Permalink
Add support for TLS and connectionless LDAP connections on Linux (#52904
Browse files Browse the repository at this point in the history
)

* Set LDAP version with pointers on Linux

* Replace deprecated OpenLDAP methods

In OpenLDAP, ldap_simple_bind_s is deprecated in favor of
ldap_sasl_bind_s with the LDAP_SASL_SIMPLE auth method[1][].
Similarly, ldap_init is deprecated in favor of ldap_initialize[2][].
The newer APIs also allows us to specify a URI to use TLS with OpenLDAP.

[1]: https://git.openldap.org/openldap/openldap/-/blob/OPENLDAP_REL_ENG_2_4_58/include/ldap.h#L1278
[2]: https://git.openldap.org/openldap/openldap//blob/OPENLDAP_REL_ENG_2_4_58/include/ldap.h#L1513

* Add TLS and connectionless LDAP support to Linux

This commit manually specifies the LDAP URI option during connect
(but before binding). This is necessary because in order to know the
correct scheme, we need access to SessionOptions, which is not available
until after initialization.

Finally, it removes the PlatformUnsupportedException from the
SessionOptions.SecureSocketLayer property.

This makes it possible to use LDAP over TLS and connectionless (UDP)
LDAP.

* Add test configuration for LDAP TLS server
  • Loading branch information
iinuwa authored Jun 3, 2021
1 parent 01a8f49 commit 3291837
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 27 deletions.
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">
<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);
}
}

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://";
}
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);
}

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")]
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();
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;
connection.Bind();

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

0 comments on commit 3291837

Please sign in to comment.