diff --git a/src/libraries/Common/src/Interop/Interop.Ldap.cs b/src/libraries/Common/src/Interop/Interop.Ldap.cs index 4ff3fd17eed9b2..84d78f6002e510 100644 --- a/src/libraries/Common/src/Interop/Interop.Ldap.cs +++ b/src/libraries/Common/src/Interop/Interop.Ldap.cs @@ -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 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 392add014ed6ad..37b79557c50a64 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 enum SaslChallengeType internal static partial class Interop { + public const string LDAP_SASL_SIMPLE = null; + internal static partial class Ldap { static Ldap() @@ -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); @@ -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); @@ -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); diff --git a/src/libraries/Common/tests/System/DirectoryServices/LDAP.Configuration.xml b/src/libraries/Common/tests/System/DirectoryServices/LDAP.Configuration.xml index a80e41b7daf34e..24e41ecd4e7ea5 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 32d3f9b277016b..a9156b8efd5bbe 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 @@ private LdapConfiguration(string serverName, string searchDn, string userName, s Password = password; Port = port; AuthenticationTypes = at; + UseTls = useTls; } private static LdapConfiguration s_ldapConfiguration = GetConfiguration("LDAP.Configuration.xml"); @@ -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 @@ -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) @@ -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) { @@ -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) 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 87219ce1ff2ff0..31750f15897f8d 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 @@ 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); 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 fa512418f6185b..02c8f0c239587b 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 @@ 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) @@ -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; 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 052ef46be40bcc..b7bcca29c0ac26 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 @@ 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)); } } } 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 50362992724b5c..c587f4218a4ca6 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 @@ 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); + } } } 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 34884b97972b0f..869c86a8900ace 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 @@ 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); @@ -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) diff --git a/src/libraries/System.DirectoryServices.Protocols/tests/DirectoryServicesProtocolsTests.cs b/src/libraries/System.DirectoryServices.Protocols/tests/DirectoryServicesProtocolsTests.cs index 00d56e12bc8d19..f13a864d000187 100644 --- a/src/libraries/System.DirectoryServices.Protocols/tests/DirectoryServicesProtocolsTests.cs +++ b/src/libraries/System.DirectoryServices.Protocols/tests/DirectoryServicesProtocolsTests.cs @@ -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);