Skip to content

[release/8.0-staging] Add support for LDAPTLS_CACERTDIR \ TrustedCertificateDirectory #112530

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/libraries/Common/src/Interop/Interop.Ldap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ internal enum LdapOption
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_TLS_CACERTDIR = 0x6003, // Not Supported in Windows
LDAP_OPT_X_TLS_NEWCTX = 0x600F, // Not Supported in Windows
LDAP_OPT_X_SASL_REALM = 0x6101,
LDAP_OPT_X_SASL_AUTHCID = 0x6102,
LDAP_OPT_X_SASL_AUTHZID = 0x6103
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ To enable the tests marked with [ConditionalFact(nameof(IsLdapConfigurationExist

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

When testing with later of versions of LDAP, the ldapsearch commands below may need to use

-H ldap://localhost:<PORT>

instead of

-h localhost -p <PORT>

OPENDJ SERVER
=============

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,8 @@ public partial class LdapSessionOptions
internal LdapSessionOptions() { }
public bool AutoReconnect { get { throw null; } set { } }
public string DomainName { get { throw null; } set { } }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")]
public string TrustedCertificatesDirectory { get { throw null; } set { } }
public string HostName { get { throw null; } set { } }
public bool HostReachable { get { throw null; } }
public System.DirectoryServices.Protocols.LocatorFlags LocatorFlag { get { throw null; } set { } }
Expand All @@ -402,6 +404,8 @@ internal LdapSessionOptions() { }
public bool Signing { get { throw null; } set { } }
public System.DirectoryServices.Protocols.SecurityPackageContextConnectionInformation SslInformation { get { throw null; } }
public int SspiFlag { get { throw null; } set { } }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")]
public void StartNewTlsSessionContext() { }
public bool TcpKeepAlive { get { throw null; } set { } }
public System.DirectoryServices.Protocols.VerifyServerCertificateCallback VerifyServerCertificate { get { throw null; } set { } }
public void FastConcurrentBind() { }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.get_TrustedCertificatesDirectory</Target>
<Left>lib/net6.0/System.DirectoryServices.Protocols.dll</Left>
<Right>lib/net6.0/System.DirectoryServices.Protocols.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.set_TrustedCertificatesDirectory(System.String)</Target>
<Left>lib/net6.0/System.DirectoryServices.Protocols.dll</Left>
<Right>lib/net6.0/System.DirectoryServices.Protocols.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.StartNewTlsSessionContext</Target>
<Left>lib/net6.0/System.DirectoryServices.Protocols.dll</Left>
<Right>lib/net6.0/System.DirectoryServices.Protocols.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.get_TrustedCertificatesDirectory</Target>
<Left>lib/net7.0/System.DirectoryServices.Protocols.dll</Left>
<Right>lib/net7.0/System.DirectoryServices.Protocols.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.set_TrustedCertificatesDirectory(System.String)</Target>
<Left>lib/net7.0/System.DirectoryServices.Protocols.dll</Left>
<Right>lib/net7.0/System.DirectoryServices.Protocols.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.StartNewTlsSessionContext</Target>
<Left>lib/net7.0/System.DirectoryServices.Protocols.dll</Left>
<Right>lib/net7.0/System.DirectoryServices.Protocols.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.get_TrustedCertificatesDirectory</Target>
<Left>lib/net8.0/System.DirectoryServices.Protocols.dll</Left>
<Right>lib/net8.0/System.DirectoryServices.Protocols.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.set_TrustedCertificatesDirectory(System.String)</Target>
<Left>lib/net8.0/System.DirectoryServices.Protocols.dll</Left>
<Right>lib/net8.0/System.DirectoryServices.Protocols.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.StartNewTlsSessionContext</Target>
<Left>lib/net8.0/System.DirectoryServices.Protocols.dll</Left>
<Right>lib/net8.0/System.DirectoryServices.Protocols.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.get_TrustedCertificatesDirectory</Target>
<Left>lib/netstandard2.0/System.DirectoryServices.Protocols.dll</Left>
<Right>lib/netstandard2.0/System.DirectoryServices.Protocols.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.set_TrustedCertificatesDirectory(System.String)</Target>
<Left>lib/netstandard2.0/System.DirectoryServices.Protocols.dll</Left>
<Right>lib/netstandard2.0/System.DirectoryServices.Protocols.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.DirectoryServices.Protocols.LdapSessionOptions.StartNewTlsSessionContext</Target>
<Left>lib/netstandard2.0/System.DirectoryServices.Protocols.dll</Left>
<Right>lib/netstandard2.0/System.DirectoryServices.Protocols.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>
Original file line number Diff line number Diff line change
Expand Up @@ -426,4 +426,7 @@
<data name="ReferralChasingOptionsNotSupported" xml:space="preserve">
<value>Only ReferralChasingOptions.None and ReferralChasingOptions.All are supported on Linux.</value>
</data>
<data name="DirectoryNotFound" xml:space="preserve">
<value>The directory '{0}' does not exist.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IncludeDllSafeSearchPathAttribute>true</IncludeDllSafeSearchPathAttribute>
<IsPackable>true</IsPackable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<ServicingVersion>1</ServicingVersion>
<AddNETFrameworkPlaceholderFileToPackage>true</AddNETFrameworkPlaceholderFileToPackage>
<AddNETFrameworkAssemblyReferenceToPackage>true</AddNETFrameworkAssemblyReferenceToPackage>
<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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.IO;
using System.Runtime.Versioning;

namespace System.DirectoryServices.Protocols
{
Expand All @@ -11,6 +13,34 @@ public partial class LdapSessionOptions

private bool _secureSocketLayer;

/// <summary>
/// Specifies the path of the directory containing CA certificates in the PEM format.
/// Multiple directories may be specified by separating with a semi-colon.
/// </summary>
/// <remarks>
/// The certificate files are looked up by the CA subject name hash value where that hash can be
/// obtained by using, for example, <code>openssl x509 -hash -noout -in CA.crt</code>.
/// It is a common practice to have the certificate file be a symbolic link to the actual certificate file
/// which can be done by using <code>openssl rehash .</code> or <code>c_rehash .</code> in the directory
/// containing the certificate files.
/// </remarks>
/// <exception cref="DirectoryNotFoundException">The directory not exist.</exception>
[UnsupportedOSPlatform("windows")]
public string TrustedCertificatesDirectory
{
get => GetStringValueHelper(LdapOption.LDAP_OPT_X_TLS_CACERTDIR, releasePtr: true);

set
{
if (!Directory.Exists(value))
{
throw new DirectoryNotFoundException(SR.Format(SR.DirectoryNotFound, value));
}

SetStringOptionHelper(LdapOption.LDAP_OPT_X_TLS_CACERTDIR, value);
}
}

public bool SecureSocketLayer
{
get
Expand Down Expand Up @@ -52,6 +82,16 @@ public ReferralChasingOptions ReferralChasing
}
}

/// <summary>
/// Create a new TLS library context.
/// Calling this is necessary after setting TLS-based options, such as <c>TrustedCertificatesDirectory</c>.
/// </summary>
[UnsupportedOSPlatform("windows")]
public void StartNewTlsSessionContext()
{
SetIntValueHelper(LdapOption.LDAP_OPT_X_TLS_NEWCTX, 0);
}

private bool GetBoolValueHelper(LdapOption option)
{
if (_connection._disposed) throw new ObjectDisposedException(GetType().Name);
Expand All @@ -71,5 +111,14 @@ private void SetBoolValueHelper(LdapOption option, bool value)

ErrorChecking.CheckAndSetLdapError(error);
}

private void SetStringOptionHelper(LdapOption option, string value)
{
if (_connection._disposed) throw new ObjectDisposedException(GetType().Name);

int error = LdapPal.SetStringOption(_connection._ldapHandle, option, value);

ErrorChecking.CheckAndSetLdapError(error);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ public partial class LdapSessionOptions
{
private static void PALCertFreeCRLContext(IntPtr certPtr) => Interop.Ldap.CertFreeCRLContext(certPtr);

[UnsupportedOSPlatform("windows")]
public string TrustedCertificatesDirectory
{
get => throw new PlatformNotSupportedException();
set => throw new PlatformNotSupportedException();
}

public bool SecureSocketLayer
{
get
Expand All @@ -24,6 +31,9 @@ public bool SecureSocketLayer
}
}

[UnsupportedOSPlatform("windows")]
public void StartNewTlsSessionContext() => throw new PlatformNotSupportedException();

public int ProtocolVersion
{
get => GetIntValueHelper(LdapOption.LDAP_OPT_VERSION);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics;
using System.DirectoryServices.Tests;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using Xunit;

namespace System.DirectoryServices.Protocols.Tests
Expand All @@ -16,7 +14,7 @@ public partial class DirectoryServicesProtocolsTests
{
internal static bool IsLdapConfigurationExist => LdapConfiguration.Configuration != null;
internal static bool IsActiveDirectoryServer => IsLdapConfigurationExist && LdapConfiguration.Configuration.IsActiveDirectoryServer;

internal static bool UseTls => IsLdapConfigurationExist && LdapConfiguration.Configuration.UseTls;
internal static bool IsServerSideSortSupported => IsLdapConfigurationExist && LdapConfiguration.Configuration.SupportsServerSideSort;

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

#if NET
[ConditionalFact(nameof(UseTls))]
[PlatformSpecific(TestPlatforms.Linux)]
public void StartNewTlsSessionContext()
{
using (var connection = GetConnection(bind: false))
{
// We use "." as the directory since it must be a valid directory for StartNewTlsSessionContext() + Bind() to be successful even
// though there are no client certificates in ".".
connection.SessionOptions.TrustedCertificatesDirectory = ".";

// For a real-world scenario, we would call 'StartTransportLayerSecurity(null)' here which would do the TLS handshake including
// providing the client certificate to the server and validating the server certificate. However, this requires additional
// setup that we don't have including trusting the server certificate and by specifying "demand" in the setup of the server
// via 'LDAP_TLS_VERIFY_CLIENT=demand' to force the TLS handshake to occur.

connection.SessionOptions.StartNewTlsSessionContext();
connection.Bind();

SearchRequest searchRequest = new (LdapConfiguration.Configuration.SearchDn, "(objectClass=*)", SearchScope.Subtree);
_ = (SearchResponse)connection.SendRequest(searchRequest);
}
}

[ConditionalFact(nameof(UseTls))]
[PlatformSpecific(TestPlatforms.Linux)]
public void StartNewTlsSessionContext_ThrowsLdapException()
{
using (var connection = GetConnection(bind: false))
{
// Create a new session context without setting TrustedCertificatesDirectory.
connection.SessionOptions.StartNewTlsSessionContext();
Assert.Throws<PlatformNotSupportedException>(() => connection.Bind());
}
}

[ConditionalFact(nameof(IsLdapConfigurationExist))]
[PlatformSpecific(TestPlatforms.Linux)]
public void TrustedCertificatesDirectory_ThrowsDirectoryNotFoundException()
{
using (var connection = GetConnection(bind: false))
{
Assert.Throws<DirectoryNotFoundException>(() => connection.SessionOptions.TrustedCertificatesDirectory = "nonexistent");
}
}

[ConditionalFact(nameof(IsLdapConfigurationExist))]
[PlatformSpecific(TestPlatforms.Windows)]
public void StartNewTlsSessionContext_ThrowsPlatformNotSupportedException()
{
using (var connection = new LdapConnection("server"))
{
LdapSessionOptions options = connection.SessionOptions;
Assert.Throws<PlatformNotSupportedException>(() => options.StartNewTlsSessionContext());
}
}
#endif

private void DeleteAttribute(LdapConnection connection, string entryDn, string attributeName)
{
string dn = entryDn + "," + LdapConfiguration.Configuration.SearchDn;
Expand Down Expand Up @@ -774,13 +830,18 @@ private SearchResultEntry SearchUser(LdapConnection connection, string rootDn, s
return null;
}

private LdapConnection GetConnection()
private LdapConnection GetConnection(bool bind = true)
{
LdapDirectoryIdentifier directoryIdentifier = string.IsNullOrEmpty(LdapConfiguration.Configuration.Port) ?
new LdapDirectoryIdentifier(LdapConfiguration.Configuration.ServerName, true, false) :
new LdapDirectoryIdentifier(LdapConfiguration.Configuration.ServerName,
int.Parse(LdapConfiguration.Configuration.Port, NumberStyles.None, CultureInfo.InvariantCulture),
true, false);
fullyQualifiedDnsHostName: true, connectionless: false);
return GetConnection(directoryIdentifier, bind);
}

private static LdapConnection GetConnection(LdapDirectoryIdentifier directoryIdentifier, bool bind = true)
{
NetworkCredential credential = new NetworkCredential(LdapConfiguration.Configuration.UserName, LdapConfiguration.Configuration.Password);

LdapConnection connection = new LdapConnection(directoryIdentifier, credential)
Expand All @@ -792,7 +853,11 @@ private LdapConnection GetConnection()
// 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();

if (bind)
{
connection.Bind();
}

connection.Timeout = new TimeSpan(0, 3, 0);
return connection;
Expand Down
Loading
Loading