Skip to content

Commit be6cf76

Browse files
authored
[release/8.0-staging] Add support for LDAPTLS_CACERTDIR \ TrustedCertificateDirectory (#112530)
1 parent e60ddb4 commit be6cf76

File tree

10 files changed

+265
-7
lines changed

10 files changed

+265
-7
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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ To enable the tests marked with [ConditionalFact(nameof(IsLdapConfigurationExist
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+
715
OPENDJ SERVER
816
=============
917

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() { }
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
3+
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
4+
<Suppression>
5+
<DiagnosticId>CP0002</DiagnosticId>
6+
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.get_TrustedCertificatesDirectory</Target>
7+
<Left>lib/net6.0/System.DirectoryServices.Protocols.dll</Left>
8+
<Right>lib/net6.0/System.DirectoryServices.Protocols.dll</Right>
9+
<IsBaselineSuppression>true</IsBaselineSuppression>
10+
</Suppression>
11+
<Suppression>
12+
<DiagnosticId>CP0002</DiagnosticId>
13+
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.set_TrustedCertificatesDirectory(System.String)</Target>
14+
<Left>lib/net6.0/System.DirectoryServices.Protocols.dll</Left>
15+
<Right>lib/net6.0/System.DirectoryServices.Protocols.dll</Right>
16+
<IsBaselineSuppression>true</IsBaselineSuppression>
17+
</Suppression>
18+
<Suppression>
19+
<DiagnosticId>CP0002</DiagnosticId>
20+
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.StartNewTlsSessionContext</Target>
21+
<Left>lib/net6.0/System.DirectoryServices.Protocols.dll</Left>
22+
<Right>lib/net6.0/System.DirectoryServices.Protocols.dll</Right>
23+
<IsBaselineSuppression>true</IsBaselineSuppression>
24+
</Suppression>
25+
<Suppression>
26+
<DiagnosticId>CP0002</DiagnosticId>
27+
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.get_TrustedCertificatesDirectory</Target>
28+
<Left>lib/net7.0/System.DirectoryServices.Protocols.dll</Left>
29+
<Right>lib/net7.0/System.DirectoryServices.Protocols.dll</Right>
30+
<IsBaselineSuppression>true</IsBaselineSuppression>
31+
</Suppression>
32+
<Suppression>
33+
<DiagnosticId>CP0002</DiagnosticId>
34+
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.set_TrustedCertificatesDirectory(System.String)</Target>
35+
<Left>lib/net7.0/System.DirectoryServices.Protocols.dll</Left>
36+
<Right>lib/net7.0/System.DirectoryServices.Protocols.dll</Right>
37+
<IsBaselineSuppression>true</IsBaselineSuppression>
38+
</Suppression>
39+
<Suppression>
40+
<DiagnosticId>CP0002</DiagnosticId>
41+
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.StartNewTlsSessionContext</Target>
42+
<Left>lib/net7.0/System.DirectoryServices.Protocols.dll</Left>
43+
<Right>lib/net7.0/System.DirectoryServices.Protocols.dll</Right>
44+
<IsBaselineSuppression>true</IsBaselineSuppression>
45+
</Suppression>
46+
<Suppression>
47+
<DiagnosticId>CP0002</DiagnosticId>
48+
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.get_TrustedCertificatesDirectory</Target>
49+
<Left>lib/net8.0/System.DirectoryServices.Protocols.dll</Left>
50+
<Right>lib/net8.0/System.DirectoryServices.Protocols.dll</Right>
51+
<IsBaselineSuppression>true</IsBaselineSuppression>
52+
</Suppression>
53+
<Suppression>
54+
<DiagnosticId>CP0002</DiagnosticId>
55+
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.set_TrustedCertificatesDirectory(System.String)</Target>
56+
<Left>lib/net8.0/System.DirectoryServices.Protocols.dll</Left>
57+
<Right>lib/net8.0/System.DirectoryServices.Protocols.dll</Right>
58+
<IsBaselineSuppression>true</IsBaselineSuppression>
59+
</Suppression>
60+
<Suppression>
61+
<DiagnosticId>CP0002</DiagnosticId>
62+
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.StartNewTlsSessionContext</Target>
63+
<Left>lib/net8.0/System.DirectoryServices.Protocols.dll</Left>
64+
<Right>lib/net8.0/System.DirectoryServices.Protocols.dll</Right>
65+
<IsBaselineSuppression>true</IsBaselineSuppression>
66+
</Suppression>
67+
<Suppression>
68+
<DiagnosticId>CP0002</DiagnosticId>
69+
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.get_TrustedCertificatesDirectory</Target>
70+
<Left>lib/netstandard2.0/System.DirectoryServices.Protocols.dll</Left>
71+
<Right>lib/netstandard2.0/System.DirectoryServices.Protocols.dll</Right>
72+
<IsBaselineSuppression>true</IsBaselineSuppression>
73+
</Suppression>
74+
<Suppression>
75+
<DiagnosticId>CP0002</DiagnosticId>
76+
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.set_TrustedCertificatesDirectory(System.String)</Target>
77+
<Left>lib/netstandard2.0/System.DirectoryServices.Protocols.dll</Left>
78+
<Right>lib/netstandard2.0/System.DirectoryServices.Protocols.dll</Right>
79+
<IsBaselineSuppression>true</IsBaselineSuppression>
80+
</Suppression>
81+
<Suppression>
82+
<DiagnosticId>CP0002</DiagnosticId>
83+
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.StartNewTlsSessionContext</Target>
84+
<Left>lib/netstandard2.0/System.DirectoryServices.Protocols.dll</Left>
85+
<Right>lib/netstandard2.0/System.DirectoryServices.Protocols.dll</Right>
86+
<IsBaselineSuppression>true</IsBaselineSuppression>
87+
</Suppression>
88+
</Suppressions>

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.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
55
<IncludeDllSafeSearchPathAttribute>true</IncludeDllSafeSearchPathAttribute>
66
<IsPackable>true</IsPackable>
7+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
8+
<ServicingVersion>1</ServicingVersion>
79
<AddNETFrameworkPlaceholderFileToPackage>true</AddNETFrameworkPlaceholderFileToPackage>
810
<AddNETFrameworkAssemblyReferenceToPackage>true</AddNETFrameworkAssemblyReferenceToPackage>
911
<PackageDescription>Provides the methods defined in the Lightweight Directory Access Protocol (LDAP) version 3 (V3) and Directory Services Markup Language (DSML) version 2.0 (V2) standards.</PackageDescription>

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;

0 commit comments

Comments
 (0)