Skip to content

Commit a3ac760

Browse files
committed
Add support for LDAPTLS_CACERTDIR \ TrustedCertificateDirectory (dotnet#111877)
1 parent 6f7daef commit a3ac760

File tree

10 files changed

+197
-28
lines changed

10 files changed

+197
-28
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ internal enum LdapOption
157157
LDAP_OPT_ROOTDSE_CACHE = 0x9a, // Not Supported in Linux
158158
LDAP_OPT_DEBUG_LEVEL = 0x5001,
159159
LDAP_OPT_URI = 0x5006, // Not Supported in Windows
160+
LDAP_OPT_X_TLS_CACERTDIR = 0x6003, // Not Supported in Windows
161+
LDAP_OPT_X_TLS_NEWCTX = 0x600F, // Not Supported in Windows
160162
LDAP_OPT_X_SASL_REALM = 0x6101,
161163
LDAP_OPT_X_SASL_AUTHCID = 0x6102,
162164
LDAP_OPT_X_SASL_AUTHZID = 0x6103

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

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Configuration>
22
<CommentThatAllowsDoubleHyphens>
3-
To enable the tests marked with [ConditionalFact(nameof(IsLdapConfigurationExist))], you need to setup an LDAP server and provide the needed server info here.
3+
To enable the tests marked with [ConditionalFact(nameof(IsLdapConfigurationExist))], you need to setup an LDAP server as described below and set the environment variable LDAP_TEST_SERVER_INDEX to the appropriate offset into the XML section found at the end of this file.
44

55
To ship, we should test on both an Active Directory LDAP server, and at least one other server, as behaviors are a little different. However for local testing, it is easiest to connect to an OpenDJ LDAP server in a docker container (eg., in WSL2).
66

@@ -11,7 +11,7 @@ OPENDJ SERVER
1111

1212
test it with this command - it should return some results in WSL2
1313

14-
ldapsearch -h localhost -p 1389 -D 'cn=admin,dc=example,dc=com' -x -w password
14+
ldapsearch -H ldap://localhost:1389 -D 'cn=admin,dc=example,dc=com' -x -w password
1515

1616
this command views the status
1717

@@ -24,16 +24,16 @@ SLAPD OPENLDAP SERVER
2424

2525
and to test and view status
2626

27-
ldapsearch -h localhost -p 390 -D 'cn=admin,dc=example,dc=com' -x -w password
27+
ldapsearch -H ldap://localhost:390 -D 'cn=admin,dc=example,dc=com' -x -w password
2828

2929
docker exec -it slapd01 slapcat
3030

3131
SLAPD OPENLDAP SERVER WITH TLS
3232
==============================
3333

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

36-
Start the container, with TLS on port 1636, without client certificate verification:
36+
Start the container, with TLS on port 1636, but without client certificate verification:
3737

3838
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
3939

@@ -56,6 +56,8 @@ To test and view the status:
5656

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

59+
use '-d 1' or '-d 2' for debugging.
60+
5961
ACTIVE DIRECTORY
6062
================
6163

@@ -65,7 +67,7 @@ When running against Active Directory from a Windows client, you should not see
6567

6668
If you are running your AD server as a VM on the same machine that you are running WSL2, you must execute this command on the host to bridge the two Hyper-V networks so that it is visible from WSL2:
6769

68-
Get-NetIPInterface | where {$_.InterfaceAlias -eq 'vEthernet (WSL)' -or $_.InterfaceAlias -eq 'vEthernet (Default Switch)'} | Set-NetIPInterface -Forwarding Enabled
70+
Get-NetIPInterface | where {$_.InterfaceAlias -eq 'vEthernet (WSL)' -or $_.InterfaceAlias -eq 'vEthernet (Default Switch)'} | Set-NetIPInterface -Forwarding Enabled
6971

7072
The WSL2 VM should now be able to see the AD VM by IP address. To make it visible by host name, it's probably easiest to just add it to /etc/hosts.
7173

@@ -82,7 +84,7 @@ Note:
8284
</CommentThatAllowsDoubleHyphens>
8385

8486
<!-- To choose a connection, set an environment variable LDAP_TEST_SERVER_INDEX
85-
to the zero-based index, eg., 0, 1, or 2
87+
to the zero-based index, eg., 0, 1, 2, or 3.
8688
If you don't set LDAP_TEST_SERVER_INDEX then tests that require a server
8789
will skip.
8890
-->
@@ -105,15 +107,6 @@ Note:
105107
<AuthenticationTypes>ServerBind,None</AuthenticationTypes>
106108
<SupportsServerSideSort>False</SupportsServerSideSort>
107109
</Connection>
108-
<Connection Name="ACTIVE DIRECTORY SERVER">
109-
<ServerName>danmose-ldap.danmose-domain.com</ServerName>
110-
<SearchDN>DC=danmose-domain,DC=com</SearchDN>
111-
<Port>389</Port>
112-
<User>danmose-domain\Administrator</User>
113-
<Password>%TESTPASSWORD%</Password>
114-
<AuthenticationTypes>ServerBind,None</AuthenticationTypes>
115-
<SupportsServerSideSort>True</SupportsServerSideSort>
116-
</Connection>
117110
<Connection Name="SLAPD OPENLDAP SERVER TLS">
118111
<ServerName>ldap.local</ServerName>
119112
<SearchDN>DC=example,DC=org</SearchDN>
@@ -124,5 +117,14 @@ Note:
124117
<UseTls>true</UseTls>
125118
<SupportsServerSideSort>False</SupportsServerSideSort>
126119
</Connection>
120+
<Connection Name="ACTIVE DIRECTORY SERVER">
121+
<ServerName>danmose-ldap.danmose-domain.com</ServerName>
122+
<SearchDN>DC=danmose-domain,DC=com</SearchDN>
123+
<Port>389</Port>
124+
<User>danmose-domain\Administrator</User>
125+
<Password>%TESTPASSWORD%</Password>
126+
<AuthenticationTypes>ServerBind,None</AuthenticationTypes>
127+
<SupportsServerSideSort>True</SupportsServerSideSort>
128+
</Connection>
127129

128130
</Configuration>

src/libraries/System.DirectoryServices.Protocols/ref/System.DirectoryServices.Protocols.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,8 @@ public partial class LdapSessionOptions
382382
internal LdapSessionOptions() { }
383383
public bool AutoReconnect { get { throw null; } set { } }
384384
public string DomainName { get { throw null; } set { } }
385+
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")]
386+
public string TrustedCertificatesDirectory { get { throw null; } set { } }
385387
public string HostName { get { throw null; } set { } }
386388
public bool HostReachable { get { throw null; } }
387389
public System.DirectoryServices.Protocols.LocatorFlags LocatorFlag { get { throw null; } set { } }
@@ -402,6 +404,8 @@ internal LdapSessionOptions() { }
402404
public bool Signing { get { throw null; } set { } }
403405
public System.DirectoryServices.Protocols.SecurityPackageContextConnectionInformation SslInformation { get { throw null; } }
404406
public int SspiFlag { get { throw null; } set { } }
407+
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")]
408+
public void StartNewTlsSessionContext() { }
405409
public bool TcpKeepAlive { get { throw null; } set { } }
406410
public System.DirectoryServices.Protocols.VerifyServerCertificateCallback VerifyServerCertificate { get { throw null; } set { } }
407411
public void FastConcurrentBind() { }

src/libraries/System.DirectoryServices.Protocols/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,4 +426,7 @@
426426
<data name="ReferralChasingOptionsNotSupported" xml:space="preserve">
427427
<value>Only ReferralChasingOptions.None and ReferralChasingOptions.All are supported on Linux.</value>
428428
</data>
429+
<data name="DirectoryNotFound" xml:space="preserve">
430+
<value>The directory '{0}' does not exist.</value>
431+
</data>
429432
</root>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -955,13 +955,13 @@ private unsafe Interop.BOOL ProcessClientCertificate(IntPtr ldapHandle, IntPtr C
955955

956956
private void Connect()
957957
{
958-
//Ccurrently ldap does not accept more than one certificate.
958+
// Currently ldap does not accept more than one certificate.
959959
if (ClientCertificates.Count > 1)
960960
{
961961
throw new InvalidOperationException(SR.InvalidClientCertificates);
962962
}
963963

964-
// Set the certificate callback routine here if user adds the certifcate to the certificate collection.
964+
// Set the certificate callback routine here if user adds the certificate to the certificate collection.
965965
if (ClientCertificates.Count != 0)
966966
{
967967
int certError = LdapPal.SetClientCertOption(_ldapHandle, LdapOption.LDAP_OPT_CLIENT_CERTIFICATE, _clientCertificateRoutine);

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.ComponentModel;
5+
using System.IO;
6+
using System.Runtime.Versioning;
57

68
namespace System.DirectoryServices.Protocols
79
{
@@ -11,6 +13,34 @@ public partial class LdapSessionOptions
1113

1214
private bool _secureSocketLayer;
1315

16+
/// <summary>
17+
/// Specifies the path of the directory containing CA certificates in the PEM format.
18+
/// Multiple directories may be specified by separating with a semi-colon.
19+
/// </summary>
20+
/// <remarks>
21+
/// The certificate files are looked up by the CA subject name hash value where that hash can be
22+
/// obtained by using, for example, <code>openssl x509 -hash -noout -in CA.crt</code>.
23+
/// It is a common practice to have the certificate file be a symbolic link to the actual certificate file
24+
/// which can be done by using <code>openssl rehash .</code> or <code>c_rehash .</code> in the directory
25+
/// containing the certificate files.
26+
/// </remarks>
27+
/// <exception cref="DirectoryNotFoundException">The directory not exist.</exception>
28+
[UnsupportedOSPlatform("windows")]
29+
public string TrustedCertificatesDirectory
30+
{
31+
get => GetStringValueHelper(LdapOption.LDAP_OPT_X_TLS_CACERTDIR, releasePtr: true);
32+
33+
set
34+
{
35+
if (!Directory.Exists(value))
36+
{
37+
throw new DirectoryNotFoundException(SR.Format(SR.DirectoryNotFound, value));
38+
}
39+
40+
SetStringOptionHelper(LdapOption.LDAP_OPT_X_TLS_CACERTDIR, value);
41+
}
42+
}
43+
1444
public bool SecureSocketLayer
1545
{
1646
get
@@ -52,6 +82,16 @@ public ReferralChasingOptions ReferralChasing
5282
}
5383
}
5484

85+
/// <summary>
86+
/// Create a new TLS library context.
87+
/// Calling this is necessary after setting TLS-based options, such as <c>TrustedCertificatesDirectory</c>.
88+
/// </summary>
89+
[UnsupportedOSPlatform("windows")]
90+
public void StartNewTlsSessionContext()
91+
{
92+
SetIntValueHelper(LdapOption.LDAP_OPT_X_TLS_NEWCTX, 0);
93+
}
94+
5595
private bool GetBoolValueHelper(LdapOption option)
5696
{
5797
if (_connection._disposed) throw new ObjectDisposedException(GetType().Name);
@@ -71,5 +111,14 @@ private void SetBoolValueHelper(LdapOption option, bool value)
71111

72112
ErrorChecking.CheckAndSetLdapError(error);
73113
}
114+
115+
private void SetStringOptionHelper(LdapOption option, string value)
116+
{
117+
if (_connection._disposed) throw new ObjectDisposedException(GetType().Name);
118+
119+
int error = LdapPal.SetStringOption(_connection._ldapHandle, option, value);
120+
121+
ErrorChecking.CheckAndSetLdapError(error);
122+
}
74123
}
75124
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ public partial class LdapSessionOptions
1010
{
1111
private static void PALCertFreeCRLContext(IntPtr certPtr) => Interop.Ldap.CertFreeCRLContext(certPtr);
1212

13+
[UnsupportedOSPlatform("windows")]
14+
public string TrustedCertificatesDirectory
15+
{
16+
get => throw new PlatformNotSupportedException();
17+
set => throw new PlatformNotSupportedException();
18+
}
19+
1320
public bool SecureSocketLayer
1421
{
1522
get
@@ -24,6 +31,9 @@ public bool SecureSocketLayer
2431
}
2532
}
2633

34+
[UnsupportedOSPlatform("windows")]
35+
public void StartNewTlsSessionContext() => throw new PlatformNotSupportedException();
36+
2737
public int ProtocolVersion
2838
{
2939
get => GetIntValueHelper(LdapOption.LDAP_OPT_VERSION);

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

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Generic;
5-
using System.Diagnostics;
65
using System.DirectoryServices.Tests;
76
using System.Globalization;
7+
using System.IO;
88
using System.Net;
9-
using System.Text;
10-
using System.Threading;
119
using Xunit;
1210

1311
namespace System.DirectoryServices.Protocols.Tests
@@ -16,7 +14,7 @@ public partial class DirectoryServicesProtocolsTests
1614
{
1715
internal static bool IsLdapConfigurationExist => LdapConfiguration.Configuration != null;
1816
internal static bool IsActiveDirectoryServer => IsLdapConfigurationExist && LdapConfiguration.Configuration.IsActiveDirectoryServer;
19-
17+
internal static bool UseTls => IsLdapConfigurationExist && LdapConfiguration.Configuration.UseTls;
2018
internal static bool IsServerSideSortSupported => IsLdapConfigurationExist && LdapConfiguration.Configuration.SupportsServerSideSort;
2119

2220
[ConditionalFact(nameof(IsLdapConfigurationExist))]
@@ -694,6 +692,64 @@ public void TestMultipleServerBind()
694692
connection.Timeout = new TimeSpan(0, 3, 0);
695693
}
696694

695+
#if NET
696+
[ConditionalFact(nameof(UseTls))]
697+
[PlatformSpecific(TestPlatforms.Linux)]
698+
public void StartNewTlsSessionContext()
699+
{
700+
using (var connection = GetConnection(bind: false))
701+
{
702+
// We use "." as the directory since it must be a valid directory for StartNewTlsSessionContext() + Bind() to be successful even
703+
// though there are no client certificates in ".".
704+
connection.SessionOptions.TrustedCertificatesDirectory = ".";
705+
706+
// For a real-world scenario, we would call 'StartTransportLayerSecurity(null)' here which would do the TLS handshake including
707+
// providing the client certificate to the server and validating the server certificate. However, this requires additional
708+
// setup that we don't have including trusting the server certificate and by specifying "demand" in the setup of the server
709+
// via 'LDAP_TLS_VERIFY_CLIENT=demand' to force the TLS handshake to occur.
710+
711+
connection.SessionOptions.StartNewTlsSessionContext();
712+
connection.Bind();
713+
714+
SearchRequest searchRequest = new (LdapConfiguration.Configuration.SearchDn, "(objectClass=*)", SearchScope.Subtree);
715+
_ = (SearchResponse)connection.SendRequest(searchRequest);
716+
}
717+
}
718+
719+
[ConditionalFact(nameof(UseTls))]
720+
[PlatformSpecific(TestPlatforms.Linux)]
721+
public void StartNewTlsSessionContext_ThrowsLdapException()
722+
{
723+
using (var connection = GetConnection(bind: false))
724+
{
725+
// Create a new session context without setting TrustedCertificatesDirectory.
726+
connection.SessionOptions.StartNewTlsSessionContext();
727+
Assert.Throws<PlatformNotSupportedException>(() => connection.Bind());
728+
}
729+
}
730+
731+
[ConditionalFact(nameof(IsLdapConfigurationExist))]
732+
[PlatformSpecific(TestPlatforms.Linux)]
733+
public void TrustedCertificatesDirectory_ThrowsDirectoryNotFoundException()
734+
{
735+
using (var connection = GetConnection(bind: false))
736+
{
737+
Assert.Throws<DirectoryNotFoundException>(() => connection.SessionOptions.TrustedCertificatesDirectory = "nonexistent");
738+
}
739+
}
740+
741+
[ConditionalFact(nameof(IsLdapConfigurationExist))]
742+
[PlatformSpecific(TestPlatforms.Windows)]
743+
public void StartNewTlsSessionContext_ThrowsPlatformNotSupportedException()
744+
{
745+
using (var connection = new LdapConnection("server"))
746+
{
747+
LdapSessionOptions options = connection.SessionOptions;
748+
Assert.Throws<PlatformNotSupportedException>(() => options.StartNewTlsSessionContext());
749+
}
750+
}
751+
#endif
752+
697753
private void DeleteAttribute(LdapConnection connection, string entryDn, string attributeName)
698754
{
699755
string dn = entryDn + "," + LdapConfiguration.Configuration.SearchDn;
@@ -774,13 +830,18 @@ private SearchResultEntry SearchUser(LdapConnection connection, string rootDn, s
774830
return null;
775831
}
776832

777-
private LdapConnection GetConnection()
833+
private LdapConnection GetConnection(bool bind = true)
778834
{
779835
LdapDirectoryIdentifier directoryIdentifier = string.IsNullOrEmpty(LdapConfiguration.Configuration.Port) ?
780836
new LdapDirectoryIdentifier(LdapConfiguration.Configuration.ServerName, true, false) :
781837
new LdapDirectoryIdentifier(LdapConfiguration.Configuration.ServerName,
782838
int.Parse(LdapConfiguration.Configuration.Port, NumberStyles.None, CultureInfo.InvariantCulture),
783-
true, false);
839+
fullyQualifiedDnsHostName: true, connectionless: false);
840+
return GetConnection(directoryIdentifier, bind);
841+
}
842+
843+
private static LdapConnection GetConnection(LdapDirectoryIdentifier directoryIdentifier, bool bind = true)
844+
{
784845
NetworkCredential credential = new NetworkCredential(LdapConfiguration.Configuration.UserName, LdapConfiguration.Configuration.Password);
785846

786847
LdapConnection connection = new LdapConnection(directoryIdentifier, credential)
@@ -792,7 +853,11 @@ private LdapConnection GetConnection()
792853
// to LDAP v2, which we do not support, and will return LDAP_PROTOCOL_ERROR
793854
connection.SessionOptions.ProtocolVersion = 3;
794855
connection.SessionOptions.SecureSocketLayer = LdapConfiguration.Configuration.UseTls;
795-
connection.Bind();
856+
857+
if (bind)
858+
{
859+
connection.Bind();
860+
}
796861

797862
connection.Timeout = new TimeSpan(0, 3, 0);
798863
return connection;

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,18 @@ public static bool IsLibLdapInstalled
3131
}
3232
else
3333
{
34-
_isLibLdapInstalled = NativeLibrary.TryLoad("libldap-2.4.so.2", out _);
34+
_isLibLdapInstalled =
35+
NativeLibrary.TryLoad("libldap.so.2", out _) ||
36+
NativeLibrary.TryLoad("libldap-2.6.so.0", out _) ||
37+
NativeLibrary.TryLoad("libldap-2.5.so.0", out _) ||
38+
NativeLibrary.TryLoad("libldap-2.4.so.2", out _);
3539
}
3640
}
37-
return _isLibLdapInstalled.Value;
3841
#else
3942
_isLibLdapInstalled = true; // In .NET Framework ldap is always installed.
40-
return _isLibLdapInstalled.Value;
4143
#endif
44+
45+
return _isLibLdapInstalled.Value;
4246
}
4347
}
4448
}

0 commit comments

Comments
 (0)