From 32918374655b5e1c3fc7810bbb0c06a0d03265b7 Mon Sep 17 00:00:00 2001 From: iinuwa Date: Thu, 3 Jun 2021 13:21:41 -0500 Subject: [PATCH] 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 --- src/libraries/Common/src/Interop/Interop.Ldap.cs | 1 + .../src/Interop/Linux/OpenLdap/Interop.Ldap.cs | 19 ++++--- .../DirectoryServices/LDAP.Configuration.xml | 37 +++++++++++++ .../System/DirectoryServices/LdapConfiguration.cs | 13 ++++- .../Protocols/Interop/LdapPal.Linux.cs | 22 +++++++- .../Protocols/ldap/LdapConnection.Linux.cs | 63 ++++++++++++++++++++-- .../Protocols/ldap/LdapSessionOptions.Linux.cs | 9 ++-- .../Protocols/ldap/LdapSessionOptions.Windows.cs | 6 +++ .../Protocols/ldap/LdapSessionOptions.cs | 33 +++++++++--- .../tests/DirectoryServicesProtocolsTests.cs | 1 + 10 files changed, 177 insertions(+), 27 deletions(-) diff --git a/src/libraries/Common/src/Interop/Interop.Ldap.cs b/src/libraries/Common/src/Interop/Interop.Ldap.cs index 4ff3fd1..84d78f6 100644 --- a/src/libraries/Common/src/Interop/Interop.Ldap.cs +++ b/src/libraries/Common/src/Interop/Interop.Ldap.cs @@ -99,6 +99,7 @@ namespace System.DirectoryServices.Protocols 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 diff --git a/src/libraries/Common/src/Interop/Linux/OpenLdap/Interop.Ldap.cs b/src/libraries/Common/src/Interop/Linux/OpenLdap/Interop.Ldap.cs index 392add0..37b7955 100644 --- a/src/libraries/Common/src/Interop/Linux/OpenLdap/Interop.Ldap.cs +++ b/src/libraries/Common/src/Interop/Linux/OpenLdap/Interop.Ldap.cs @@ -61,6 +61,8 @@ internal delegate int LDAP_SASL_INTERACT_PROC(IntPtr ld, uint flags, IntPtr defa internal static partial class Interop { + public const string LDAP_SASL_SIMPLE = null; + internal static partial class Ldap { static Ldap() @@ -75,10 +77,7 @@ internal static partial class Interop } [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); @@ -126,6 +125,9 @@ internal static partial class Interop 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); [DllImport(Libraries.OpenLdap, EntryPoint = "ldap_start_tls_s", CharSet = CharSet.Ansi)] @@ -143,15 +145,12 @@ internal static partial class Interop [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); diff --git a/src/libraries/Common/tests/System/DirectoryServices/LDAP.Configuration.xml b/src/libraries/Common/tests/System/DirectoryServices/LDAP.Configuration.xml index a80e41b..24e41ec 100644 --- a/src/libraries/Common/tests/System/DirectoryServices/LDAP.Configuration.xml +++ b/src/libraries/Common/tests/System/DirectoryServices/LDAP.Configuration.xml @@ -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 ================ @@ -83,5 +111,14 @@ Note: %TESTPASSWORD% ServerBind,None + + ldap.local + DC=example,DC=org + 1636 + cn=admin,dc=example,dc=org + password + ServerBind,None + true + \ No newline at end of file diff --git a/src/libraries/Common/tests/System/DirectoryServices/LdapConfiguration.cs b/src/libraries/Common/tests/System/DirectoryServices/LdapConfiguration.cs index 32d3f9b..a9156b8 100644 --- a/src/libraries/Common/tests/System/DirectoryServices/LdapConfiguration.cs +++ b/src/libraries/Common/tests/System/DirectoryServices/LdapConfiguration.cs @@ -10,7 +10,7 @@ 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; @@ -18,6 +18,7 @@ namespace System.DirectoryServices.Tests Password = password; Port = port; AuthenticationTypes = at; + UseTls = useTls; } private static LdapConfiguration s_ldapConfiguration = GetConfiguration("LDAP.Configuration.xml"); @@ -30,6 +31,7 @@ namespace System.DirectoryServices.Tests 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 @@ -104,6 +106,7 @@ namespace System.DirectoryServices.Tests string user = ""; string password = ""; AuthenticationTypes at = AuthenticationTypes.None; + bool useTls = false; XElement child = connection.Element("ServerName"); if (child != null) @@ -132,6 +135,12 @@ namespace System.DirectoryServices.Tests password = val; } + child = connection.Element("UseTls"); + if (child != null) + { + useTls = bool.Parse(child.Value); + } + child = connection.Element("AuthenticationTypes"); if (child != null) { @@ -161,7 +170,7 @@ namespace System.DirectoryServices.Tests 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) diff --git a/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/Interop/LdapPal.Linux.cs b/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/Interop/LdapPal.Linux.cs index 87219ce..31750f1 100644 --- a/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/Interop/LdapPal.Linux.cs +++ b/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/Interop/LdapPal.Linux.cs @@ -92,12 +92,32 @@ namespace System.DirectoryServices.Protocols 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); diff --git a/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapConnection.Linux.cs b/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapConnection.Linux.cs index fa51241..02c8f0c 100644 --- a/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapConnection.Linux.cs +++ b/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapConnection.Linux.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Net; +using System.Text; using System.Runtime.InteropServices; namespace System.DirectoryServices.Protocols @@ -12,13 +13,67 @@ namespace System.DirectoryServices.Protocols // 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) @@ -30,7 +85,7 @@ namespace System.DirectoryServices.Protocols } else { - error = Interop.Ldap.ldap_simple_bind(_ldapHandle, cred.user, cred.password); + error = LdapPal.BindToDirectory(_ldapHandle, cred.user, cred.password); } return error; diff --git a/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapSessionOptions.Linux.cs b/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapSessionOptions.Linux.cs index 052ef46..b7bcca2 100644 --- a/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapSessionOptions.Linux.cs +++ b/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapSessionOptions.Linux.cs @@ -9,11 +9,12 @@ namespace System.DirectoryServices.Protocols { 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)); } } } diff --git a/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapSessionOptions.Windows.cs b/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapSessionOptions.Windows.cs index 5036299..c587f42 100644 --- a/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapSessionOptions.Windows.cs +++ b/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapSessionOptions.Windows.cs @@ -23,5 +23,11 @@ namespace System.DirectoryServices.Protocols SetIntValueHelper(LdapOption.LDAP_OPT_SSL, temp); } } + + public int ProtocolVersion + { + get => GetIntValueHelper(LdapOption.LDAP_OPT_VERSION); + set => SetIntValueHelper(LdapOption.LDAP_OPT_VERSION, value); + } } } diff --git a/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapSessionOptions.cs b/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapSessionOptions.cs index 34884b9..869c86a 100644 --- a/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapSessionOptions.cs +++ b/src/libraries/System.DirectoryServices.Protocols/src/System/DirectoryServices/Protocols/ldap/LdapSessionOptions.cs @@ -168,12 +168,6 @@ namespace System.DirectoryServices.Protocols } } - 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); @@ -787,6 +781,33 @@ namespace System.DirectoryServices.Protocols 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) diff --git a/src/libraries/System.DirectoryServices.Protocols/tests/DirectoryServicesProtocolsTests.cs b/src/libraries/System.DirectoryServices.Protocols/tests/DirectoryServicesProtocolsTests.cs index 00d56e1..f13a864 100644 --- a/src/libraries/System.DirectoryServices.Protocols/tests/DirectoryServicesProtocolsTests.cs +++ b/src/libraries/System.DirectoryServices.Protocols/tests/DirectoryServicesProtocolsTests.cs @@ -630,6 +630,7 @@ namespace System.DirectoryServices.Protocols.Tests // 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); -- 2.7.4