Skip to content

Commit 3291837

Browse files
authored
Add support for TLS and connectionless LDAP connections on Linux (#52904)
* 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
1 parent 01a8f49 commit 3291837

File tree

10 files changed

+177
-27
lines changed

10 files changed

+177
-27
lines changed

src/libraries/Common/src/Interop/Interop.Ldap.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ internal enum LdapOption
9999
LDAP_OPT_SECURITY_CONTEXT = 0x99,
100100
LDAP_OPT_ROOTDSE_CACHE = 0x9a, // Not Supported in Linux
101101
LDAP_OPT_DEBUG_LEVEL = 0x5001,
102+
LDAP_OPT_URI = 0x5006, // Not Supported in Windows
102103
LDAP_OPT_X_SASL_REALM = 0x6101,
103104
LDAP_OPT_X_SASL_AUTHCID = 0x6102,
104105
LDAP_OPT_X_SASL_AUTHZID = 0x6103

src/libraries/Common/src/Interop/Linux/OpenLdap/Interop.Ldap.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ internal enum SaslChallengeType
6161

6262
internal static partial class Interop
6363
{
64+
public const string LDAP_SASL_SIMPLE = null;
65+
6466
internal static partial class Ldap
6567
{
6668
static Ldap()
@@ -75,10 +77,7 @@ static Ldap()
7577
}
7678

7779
[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_initialize", CharSet = CharSet.Ansi, SetLastError = true)]
78-
public static extern int ldap_initialize(out IntPtr ld, string hostname);
79-
80-
[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_init", CharSet = CharSet.Ansi, SetLastError = true)]
81-
public static extern IntPtr ldap_init(string hostName, int portNumber);
80+
public static extern int ldap_initialize(out IntPtr ld, string uri);
8281

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

127+
[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_set_option", CharSet = CharSet.Ansi)]
128+
public static extern int ldap_set_option_string([In] ConnectionHandle ldapHandle, [In] LdapOption option, string inValue);
129+
128130
[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_set_option", CharSet = CharSet.Ansi)]
129131
public static extern int ldap_set_option_referral([In] ConnectionHandle ldapHandle, [In] LdapOption option, ref LdapReferralCallback outValue);
130132

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

148+
[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_sasl_bind_s", CharSet = CharSet.Ansi)]
149+
internal static extern int ldap_sasl_bind([In] ConnectionHandle ld, string dn, string mechanism, berval cred, IntPtr serverctrls, IntPtr clientctrls, IntPtr servercredp);
150+
146151
[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_sasl_interactive_bind_s", CharSet = CharSet.Ansi)]
147152
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);
148153

149-
[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_simple_bind_s", CharSet = CharSet.Ansi, SetLastError = true)]
150-
public static extern int ldap_simple_bind([In] ConnectionHandle ld, string who, string passwd);
151-
152-
[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_bind_s", CharSet = CharSet.Ansi, SetLastError = true)]
153-
public static extern int ldap_bind_s([In] ConnectionHandle ld, string who, string passwd, int method);
154-
155154
[DllImport(Libraries.OpenLdap, EntryPoint = "ldap_err2string", CharSet = CharSet.Ansi)]
156155
public static extern IntPtr ldap_err2string(int err);
157156

src/libraries/Common/tests/System/DirectoryServices/LDAP.Configuration.xml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,34 @@ and to test and view status
2828

2929
docker exec -it slapd01 slapcat
3030

31+
SLAPD OPENLDAP SERVER WITH TLS
32+
==============================
33+
34+
The osixia/openldap container image automatically creates a TLS lisener with a self-signed certificate. This can be used to test TLS.
35+
36+
Start the container, with TLS on port 1636, without client certificate verification:
37+
38+
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
39+
40+
Extract the CA certificate and write to a temporary file:
41+
42+
docker exec ldap cat /container/service/slapd/assets/certs/ca.crt > /tmp/ca.crt
43+
44+
Set the LDAP client CA certificate path in `/etc/ldap/ldap.conf` so OpenLDAP trusts the self-signed certificate:
45+
46+
# /etc/ldap/ldap.conf
47+
#...
48+
TLS_CACERT /tmp/ca.crt
49+
50+
Finally, map the `ldap.local` hostname manually set above to the loopback address:
51+
52+
# /etc/hosts
53+
127.0.0.1 ldap.local
54+
55+
To test and view the status:
56+
57+
ldapsearch -H ldaps://ldap.local:1636 -b dc=example,dc=org -x -D cn=admin,dc=example,dc=org -w password
58+
3159
ACTIVE DIRECTORY
3260
================
3361

@@ -83,5 +111,14 @@ Note:
83111
<Password>%TESTPASSWORD%</Password>
84112
<AuthenticationTypes>ServerBind,None</AuthenticationTypes>
85113
</Connection>
114+
<Connection Name="SLAPD OPENLDAP SERVER TLS">
115+
<ServerName>ldap.local</ServerName>
116+
<SearchDN>DC=example,DC=org</SearchDN>
117+
<Port>1636</Port>
118+
<User>cn=admin,dc=example,dc=org</User>
119+
<Password>password</Password>
120+
<AuthenticationTypes>ServerBind,None</AuthenticationTypes>
121+
<UseTls>true</UseTls>
122+
</Connection>
86123

87124
</Configuration>

src/libraries/Common/tests/System/DirectoryServices/LdapConfiguration.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ namespace System.DirectoryServices.Tests
1010
{
1111
internal class LdapConfiguration
1212
{
13-
private LdapConfiguration(string serverName, string searchDn, string userName, string password, string port, AuthenticationTypes at)
13+
private LdapConfiguration(string serverName, string searchDn, string userName, string password, string port, AuthenticationTypes at, bool useTls)
1414
{
1515
ServerName = serverName;
1616
SearchDn = searchDn;
1717
UserName = userName;
1818
Password = password;
1919
Port = port;
2020
AuthenticationTypes = at;
21+
UseTls = useTls;
2122
}
2223

2324
private static LdapConfiguration s_ldapConfiguration = GetConfiguration("LDAP.Configuration.xml");
@@ -30,6 +31,7 @@ private LdapConfiguration(string serverName, string searchDn, string userName, s
3031
internal string Port { get; set; }
3132
internal string SearchDn { get; set; }
3233
internal AuthenticationTypes AuthenticationTypes { get; set; }
34+
internal bool UseTls { get; set; }
3335
internal string LdapPath => string.IsNullOrEmpty(Port) ? $"LDAP://{ServerName}/{SearchDn}" : $"LDAP://{ServerName}:{Port}/{SearchDn}";
3436
internal string RootDSEPath => string.IsNullOrEmpty(Port) ? $"LDAP://{ServerName}/rootDSE" : $"LDAP://{ServerName}:{Port}/rootDSE";
3537
internal string UserNameWithNoDomain
@@ -104,6 +106,7 @@ internal static LdapConfiguration GetConfiguration(string configFile)
104106
string user = "";
105107
string password = "";
106108
AuthenticationTypes at = AuthenticationTypes.None;
109+
bool useTls = false;
107110

108111
XElement child = connection.Element("ServerName");
109112
if (child != null)
@@ -132,6 +135,12 @@ internal static LdapConfiguration GetConfiguration(string configFile)
132135
password = val;
133136
}
134137

138+
child = connection.Element("UseTls");
139+
if (child != null)
140+
{
141+
useTls = bool.Parse(child.Value);
142+
}
143+
135144
child = connection.Element("AuthenticationTypes");
136145
if (child != null)
137146
{
@@ -161,7 +170,7 @@ internal static LdapConfiguration GetConfiguration(string configFile)
161170
at |= AuthenticationTypes.Signing;
162171
}
163172

164-
ldapConfig = new LdapConfiguration(serverName, searchDn, user, password, port, at);
173+
ldapConfig = new LdapConfiguration(serverName, searchDn, user, password, port, at, useTls);
165174
}
166175
}
167176
catch (Exception ex)

src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/Interop/LdapPal.Linux.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,32 @@ internal static int SearchDirectory(ConnectionHandle ldapHandle, string dn, int
9292

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

95+
internal static int SetStringOption(ConnectionHandle ldapHandle, LdapOption option, string inValue) => Interop.Ldap.ldap_set_option_string(ldapHandle, option, inValue);
96+
9597
internal static int SetReferralOption(ConnectionHandle ldapHandle, LdapOption option, ref LdapReferralCallback outValue) => Interop.Ldap.ldap_set_option_referral(ldapHandle, option, ref outValue);
9698

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

100-
internal static int BindToDirectory(ConnectionHandle ld, string who, string passwd) => Interop.Ldap.ldap_simple_bind(ld, who, passwd);
102+
internal static int BindToDirectory(ConnectionHandle ld, string who, string passwd)
103+
{
104+
IntPtr passwordPtr = IntPtr.Zero;
105+
try
106+
{
107+
passwordPtr = LdapPal.StringToPtr(passwd);
108+
berval passwordBerval = new berval
109+
{
110+
bv_len = passwd.Length,
111+
bv_val = passwordPtr,
112+
};
113+
114+
return Interop.Ldap.ldap_sasl_bind(ld, who, Interop.LDAP_SASL_SIMPLE, passwordBerval, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
115+
}
116+
finally
117+
{
118+
Marshal.FreeHGlobal(passwordPtr);
119+
}
120+
}
101121

102122
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);
103123

src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapConnection.Linux.cs

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Diagnostics;
55
using System.Net;
6+
using System.Text;
67
using System.Runtime.InteropServices;
78

89
namespace System.DirectoryServices.Protocols
@@ -12,13 +13,67 @@ public partial class LdapConnection
1213
// 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.
1314
private bool _setFQDNDone = true;
1415

15-
private void InternalInitConnectionHandle(string hostname) => _ldapHandle = new ConnectionHandle(Interop.Ldap.ldap_init(hostname, ((LdapDirectoryIdentifier)_directoryIdentifier).PortNumber), _needDispose);
16+
private void InternalInitConnectionHandle(string hostname)
17+
{
18+
if ((LdapDirectoryIdentifier)_directoryIdentifier == null)
19+
{
20+
throw new NullReferenceException();
21+
}
22+
23+
_ldapHandle = new ConnectionHandle();
24+
}
1625

1726
private int InternalConnectToServer()
1827
{
28+
// In Linux you don't have to call Connect after calling init. You
29+
// directly call bind. However, we set the URI for the connection
30+
// here instead of during initialization because we need access to
31+
// the SessionOptions property to properly define it, which is not
32+
// available during init.
1933
Debug.Assert(!_ldapHandle.IsInvalid);
20-
// In Linux you don't have to call Connect after calling init. You directly call bind.
21-
return 0;
34+
35+
string scheme = null;
36+
LdapDirectoryIdentifier directoryIdentifier = (LdapDirectoryIdentifier)_directoryIdentifier;
37+
if (directoryIdentifier.Connectionless)
38+
{
39+
scheme = "cldap://";
40+
}
41+
else if (SessionOptions.SecureSocketLayer)
42+
{
43+
scheme = "ldaps://";
44+
}
45+
else
46+
{
47+
scheme = "ldap://";
48+
}
49+
50+
string uris = null;
51+
string[] servers = directoryIdentifier.Servers;
52+
if (servers != null && servers.Length != 0)
53+
{
54+
StringBuilder temp = new StringBuilder(200);
55+
for (int i = 0; i < servers.Length; i++)
56+
{
57+
if (i != 0)
58+
{
59+
temp.Append(' ');
60+
}
61+
temp.Append(scheme);
62+
temp.Append(servers[i]);
63+
temp.Append(':');
64+
temp.Append(directoryIdentifier.PortNumber);
65+
}
66+
if (temp.Length != 0)
67+
{
68+
uris = temp.ToString();
69+
}
70+
}
71+
else
72+
{
73+
uris = $"{scheme}:{directoryIdentifier.PortNumber}";
74+
}
75+
76+
return LdapPal.SetStringOption(_ldapHandle, LdapOption.LDAP_OPT_URI, uris);
2277
}
2378

2479
private int InternalBind(NetworkCredential tempCredential, SEC_WINNT_AUTH_IDENTITY_EX cred, BindMethod method)
@@ -30,7 +85,7 @@ private int InternalBind(NetworkCredential tempCredential, SEC_WINNT_AUTH_IDENTI
3085
}
3186
else
3287
{
33-
error = Interop.Ldap.ldap_simple_bind(_ldapHandle, cred.user, cred.password);
88+
error = LdapPal.BindToDirectory(_ldapHandle, cred.user, cred.password);
3489
}
3590

3691
return error;

src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapSessionOptions.Linux.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ public partial class LdapSessionOptions
99
{
1010
private static void PALCertFreeCRLContext(IntPtr certPtr) { /* No op */ }
1111

12-
[SupportedOSPlatform("windows")]
13-
public bool SecureSocketLayer
12+
public bool SecureSocketLayer { get; set; }
13+
14+
public int ProtocolVersion
1415
{
15-
get => throw new PlatformNotSupportedException();
16-
set => throw new PlatformNotSupportedException();
16+
get => GetPtrValueHelper(LdapOption.LDAP_OPT_VERSION).ToInt32();
17+
set => SetPtrValueHelper(LdapOption.LDAP_OPT_VERSION, new IntPtr(value));
1718
}
1819
}
1920
}

src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapSessionOptions.Windows.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,11 @@ public bool SecureSocketLayer
2323
SetIntValueHelper(LdapOption.LDAP_OPT_SSL, temp);
2424
}
2525
}
26+
27+
public int ProtocolVersion
28+
{
29+
get => GetIntValueHelper(LdapOption.LDAP_OPT_VERSION);
30+
set => SetIntValueHelper(LdapOption.LDAP_OPT_VERSION, value);
31+
}
2632
}
2733
}

src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapSessionOptions.cs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,6 @@ public int ReferralHopLimit
168168
}
169169
}
170170

171-
public int ProtocolVersion
172-
{
173-
get => GetIntValueHelper(LdapOption.LDAP_OPT_VERSION);
174-
set => SetIntValueHelper(LdapOption.LDAP_OPT_VERSION, value);
175-
}
176-
177171
public string HostName
178172
{
179173
get => GetStringValueHelper(LdapOption.LDAP_OPT_HOST_NAME, false);
@@ -787,6 +781,33 @@ private void SetIntValueHelper(LdapOption option, int value)
787781
ErrorChecking.CheckAndSetLdapError(error);
788782
}
789783

784+
private IntPtr GetPtrValueHelper(LdapOption option)
785+
{
786+
if (_connection._disposed)
787+
{
788+
throw new ObjectDisposedException(GetType().Name);
789+
}
790+
791+
IntPtr outValue = new IntPtr(0);
792+
int error = LdapPal.GetPtrOption(_connection._ldapHandle, option, ref outValue);
793+
ErrorChecking.CheckAndSetLdapError(error);
794+
795+
return outValue;
796+
}
797+
798+
private void SetPtrValueHelper(LdapOption option, IntPtr value)
799+
{
800+
if (_connection._disposed)
801+
{
802+
throw new ObjectDisposedException(GetType().Name);
803+
}
804+
805+
IntPtr temp = value;
806+
int error = LdapPal.SetPtrOption(_connection._ldapHandle, option, ref temp);
807+
808+
ErrorChecking.CheckAndSetLdapError(error);
809+
}
810+
790811
private string GetStringValueHelper(LdapOption option, bool releasePtr)
791812
{
792813
if (_connection._disposed)

src/libraries/System.DirectoryServices.Protocols/tests/DirectoryServicesProtocolsTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,7 @@ private LdapConnection GetConnection()
630630
// Set server protocol before bind; OpenLDAP servers default
631631
// to LDAP v2, which we do not support, and will return LDAP_PROTOCOL_ERROR
632632
connection.SessionOptions.ProtocolVersion = 3;
633+
connection.SessionOptions.SecureSocketLayer = LdapConfiguration.Configuration.UseTls;
633634
connection.Bind();
634635

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

0 commit comments

Comments
 (0)