Skip to content

Commit 53a3e33

Browse files
authored
Add support for LDAPTLS_CACERTDIR \ TrustedCertificateDirectory (#111877)
1 parent fae05be commit 53a3e33

File tree

11 files changed

+195
-38
lines changed

11 files changed

+195
-38
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 & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,17 @@
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

7-
When testing with later of versions of LDAP, the ldapsearch commands below may need to use
8-
9-
-H ldap://localhost:<PORT>
10-
11-
instead of
12-
13-
-h localhost -p <PORT>
14-
157
OPENDJ SERVER
168
=============
179

1810
docker run -p 1389:1389 -e ROOT_USER_DN='cn=admin,dc=example,dc=com' -e BASE_DN='dc=example,dc=com' -e ROOT_PASSWORD=password -d openidentityplatform/opendj
1911

2012
test it with this command - it should return some results in WSL2
2113

22-
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
2315

2416
this command views the status
2517

@@ -32,16 +24,16 @@ SLAPD OPENLDAP SERVER
3224

3325
and to test and view status
3426

35-
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
3628

3729
docker exec -it slapd01 slapcat
3830

3931
SLAPD OPENLDAP SERVER WITH TLS
4032
==============================
4133

42-
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.
4335

44-
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:
4537

4638
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
4739

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

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

59+
use '-d 1' or '-d 2' for debugging.
60+
6761
ACTIVE DIRECTORY
6862
================
6963

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

7468
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:
7569

76-
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
7771

7872
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.
7973

@@ -90,7 +84,7 @@ Note:
9084
</CommentThatAllowsDoubleHyphens>
9185

9286
<!-- To choose a connection, set an environment variable LDAP_TEST_SERVER_INDEX
93-
to the zero-based index, eg., 0, 1, or 2
87+
to the zero-based index, eg., 0, 1, 2, or 3.
9488
If you don't set LDAP_TEST_SERVER_INDEX then tests that require a server
9589
will skip.
9690
-->
@@ -113,15 +107,6 @@ Note:
113107
<AuthenticationTypes>ServerBind,None</AuthenticationTypes>
114108
<SupportsServerSideSort>False</SupportsServerSideSort>
115109
</Connection>
116-
<Connection Name="ACTIVE DIRECTORY SERVER">
117-
<ServerName>danmose-ldap.danmose-domain.com</ServerName>
118-
<SearchDN>DC=danmose-domain,DC=com</SearchDN>
119-
<Port>389</Port>
120-
<User>danmose-domain\Administrator</User>
121-
<Password>%TESTPASSWORD%</Password>
122-
<AuthenticationTypes>ServerBind,None</AuthenticationTypes>
123-
<SupportsServerSideSort>True</SupportsServerSideSort>
124-
</Connection>
125110
<Connection Name="SLAPD OPENLDAP SERVER TLS">
126111
<ServerName>ldap.local</ServerName>
127112
<SearchDN>DC=example,DC=org</SearchDN>
@@ -132,5 +117,14 @@ Note:
132117
<UseTls>true</UseTls>
133118
<SupportsServerSideSort>False</SupportsServerSideSort>
134119
</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>
135129

136130
</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: 69 additions & 8 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,6 +14,7 @@ public partial class DirectoryServicesProtocolsTests
1614
{
1715
internal static bool LdapConfigurationExists => LdapConfiguration.Configuration != null;
1816
internal static bool IsActiveDirectoryServer => LdapConfigurationExists && LdapConfiguration.Configuration.IsActiveDirectoryServer;
17+
internal static bool UseTls => LdapConfigurationExists && LdapConfiguration.Configuration.UseTls;
1918

2019
internal static bool IsServerSideSortSupported => LdapConfigurationExists && LdapConfiguration.Configuration.SupportsServerSideSort;
2120

@@ -706,6 +705,64 @@ public void TestMultipleServerBind()
706705
connection.Timeout = new TimeSpan(0, 3, 0);
707706
}
708707

708+
#if NET
709+
[ConditionalFact(nameof(UseTls))]
710+
[PlatformSpecific(TestPlatforms.Linux)]
711+
public void StartNewTlsSessionContext()
712+
{
713+
using (var connection = GetConnection(bind: false))
714+
{
715+
// We use "." as the directory since it must be a valid directory for StartNewTlsSessionContext() + Bind() to be successful even
716+
// though there are no client certificates in ".".
717+
connection.SessionOptions.TrustedCertificatesDirectory = ".";
718+
719+
// For a real-world scenario, we would call 'StartTransportLayerSecurity(null)' here which would do the TLS handshake including
720+
// providing the client certificate to the server and validating the server certificate. However, this requires additional
721+
// setup that we don't have including trusting the server certificate and by specifying "demand" in the setup of the server
722+
// via 'LDAP_TLS_VERIFY_CLIENT=demand' to force the TLS handshake to occur.
723+
724+
connection.SessionOptions.StartNewTlsSessionContext();
725+
connection.Bind();
726+
727+
SearchRequest searchRequest = new (LdapConfiguration.Configuration.SearchDn, "(objectClass=*)", SearchScope.Subtree);
728+
_ = (SearchResponse)connection.SendRequest(searchRequest);
729+
}
730+
}
731+
732+
[ConditionalFact(nameof(UseTls))]
733+
[PlatformSpecific(TestPlatforms.Linux)]
734+
public void StartNewTlsSessionContext_ThrowsLdapException()
735+
{
736+
using (var connection = GetConnection(bind: false))
737+
{
738+
// Create a new session context without setting TrustedCertificatesDirectory.
739+
connection.SessionOptions.StartNewTlsSessionContext();
740+
Assert.Throws<PlatformNotSupportedException>(() => connection.Bind());
741+
}
742+
}
743+
744+
[ConditionalFact(nameof(LdapConfigurationExists))]
745+
[PlatformSpecific(TestPlatforms.Linux)]
746+
public void TrustedCertificatesDirectory_ThrowsDirectoryNotFoundException()
747+
{
748+
using (var connection = GetConnection(bind: false))
749+
{
750+
Assert.Throws<DirectoryNotFoundException>(() => connection.SessionOptions.TrustedCertificatesDirectory = "nonexistent");
751+
}
752+
}
753+
754+
[ConditionalFact(nameof(LdapConfigurationExists))]
755+
[PlatformSpecific(TestPlatforms.Windows)]
756+
public void StartNewTlsSessionContext_ThrowsPlatformNotSupportedException()
757+
{
758+
using (var connection = new LdapConnection("server"))
759+
{
760+
LdapSessionOptions options = connection.SessionOptions;
761+
Assert.Throws<PlatformNotSupportedException>(() => options.StartNewTlsSessionContext());
762+
}
763+
}
764+
#endif
765+
709766
private void DeleteAttribute(LdapConnection connection, string entryDn, string attributeName)
710767
{
711768
string dn = entryDn + "," + LdapConfiguration.Configuration.SearchDn;
@@ -786,24 +843,24 @@ private SearchResultEntry SearchUser(LdapConnection connection, string rootDn, s
786843
return null;
787844
}
788845

789-
private LdapConnection GetConnection(string server)
846+
private static LdapConnection GetConnection(string server)
790847
{
791848
LdapDirectoryIdentifier directoryIdentifier = new LdapDirectoryIdentifier(server, fullyQualifiedDnsHostName: true, connectionless: false);
792849

793850
return GetConnection(directoryIdentifier);
794851
}
795852

796-
private LdapConnection GetConnection()
853+
private static LdapConnection GetConnection(bool bind = true)
797854
{
798855
LdapDirectoryIdentifier directoryIdentifier = string.IsNullOrEmpty(LdapConfiguration.Configuration.Port) ?
799856
new LdapDirectoryIdentifier(LdapConfiguration.Configuration.ServerName, fullyQualifiedDnsHostName: true, connectionless: false) :
800857
new LdapDirectoryIdentifier(LdapConfiguration.Configuration.ServerName,
801858
int.Parse(LdapConfiguration.Configuration.Port, NumberStyles.None, CultureInfo.InvariantCulture),
802859
fullyQualifiedDnsHostName: true, connectionless: false);
803-
return GetConnection(directoryIdentifier);
860+
return GetConnection(directoryIdentifier, bind);
804861
}
805862

806-
private static LdapConnection GetConnection(LdapDirectoryIdentifier directoryIdentifier)
863+
private static LdapConnection GetConnection(LdapDirectoryIdentifier directoryIdentifier, bool bind = true)
807864
{
808865
NetworkCredential credential = new NetworkCredential(LdapConfiguration.Configuration.UserName, LdapConfiguration.Configuration.Password);
809866

@@ -816,7 +873,11 @@ private static LdapConnection GetConnection(LdapDirectoryIdentifier directoryIde
816873
// to LDAP v2, which we do not support, and will return LDAP_PROTOCOL_ERROR
817874
connection.SessionOptions.ProtocolVersion = 3;
818875
connection.SessionOptions.SecureSocketLayer = LdapConfiguration.Configuration.UseTls;
819-
connection.Bind();
876+
877+
if (bind)
878+
{
879+
connection.Bind();
880+
}
820881

821882
connection.Timeout = new TimeSpan(0, 3, 0);
822883
return connection;

0 commit comments

Comments
 (0)