Skip to content
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

[release/9.0-staging] Add support for LDAPTLS_CACERTDIR \ TrustedCertificateDirectory #112531

Open
wants to merge 2 commits into
base: release/9.0-staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
@@ -1,6 +1,6 @@
<Configuration>
<CommentThatAllowsDoubleHyphens>
To enable the tests marked with [ConditionalFact(nameof(IsLdapConfigurationExist))], you need to setup an LDAP server and provide the needed server info here.
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.

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

Expand All @@ -11,7 +11,7 @@ OPENDJ SERVER

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

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

this command views the status

Expand All @@ -24,16 +24,16 @@ SLAPD OPENLDAP SERVER

and to test and view status

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

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.
The osixia/openldap container image automatically creates a TLS listener with a self-signed certificate. This can be used to test TLS.

Start the container, with TLS on port 1636, without client certificate verification:
Start the container, with TLS on port 1636, but 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

Expand All @@ -56,6 +56,8 @@ 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

use '-d 1' or '-d 2' for debugging.

ACTIVE DIRECTORY
================

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

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:

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

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.

Expand All @@ -82,7 +84,7 @@ Note:
</CommentThatAllowsDoubleHyphens>

<!-- To choose a connection, set an environment variable LDAP_TEST_SERVER_INDEX
to the zero-based index, eg., 0, 1, or 2
to the zero-based index, eg., 0, 1, 2, or 3.
If you don't set LDAP_TEST_SERVER_INDEX then tests that require a server
will skip.
-->
Expand All @@ -105,15 +107,6 @@ Note:
<AuthenticationTypes>ServerBind,None</AuthenticationTypes>
<SupportsServerSideSort>False</SupportsServerSideSort>
</Connection>
<Connection Name="ACTIVE DIRECTORY SERVER">
<ServerName>danmose-ldap.danmose-domain.com</ServerName>
<SearchDN>DC=danmose-domain,DC=com</SearchDN>
<Port>389</Port>
<User>danmose-domain\Administrator</User>
<Password>%TESTPASSWORD%</Password>
<AuthenticationTypes>ServerBind,None</AuthenticationTypes>
<SupportsServerSideSort>True</SupportsServerSideSort>
</Connection>
<Connection Name="SLAPD OPENLDAP SERVER TLS">
<ServerName>ldap.local</ServerName>
<SearchDN>DC=example,DC=org</SearchDN>
Expand All @@ -124,5 +117,14 @@ Note:
<UseTls>true</UseTls>
<SupportsServerSideSort>False</SupportsServerSideSort>
</Connection>
<Connection Name="ACTIVE DIRECTORY SERVER">
<ServerName>danmose-ldap.danmose-domain.com</ServerName>
<SearchDN>DC=danmose-domain,DC=com</SearchDN>
<Port>389</Port>
<User>danmose-domain\Administrator</User>
<Password>%TESTPASSWORD%</Password>
<AuthenticationTypes>ServerBind,None</AuthenticationTypes>
<SupportsServerSideSort>True</SupportsServerSideSort>
</Connection>

</Configuration>
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
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 @@ -955,13 +955,13 @@ private unsafe Interop.BOOL ProcessClientCertificate(IntPtr ldapHandle, IntPtr C

private void Connect()
{
//Ccurrently ldap does not accept more than one certificate.
// Currently ldap does not accept more than one certificate.
if (ClientCertificates.Count > 1)
{
throw new InvalidOperationException(SR.InvalidClientCertificates);
}

// Set the certificate callback routine here if user adds the certifcate to the certificate collection.
// Set the certificate callback routine here if user adds the certificate to the certificate collection.
if (ClientCertificates.Count != 0)
{
int certError = LdapPal.SetClientCertOption(_ldapHandle, LdapOption.LDAP_OPT_CLIENT_CERTIFICATE, _clientCertificateRoutine);
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>
Copy link
Preview

Copilot AI Feb 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor grammar issue: 'The directory not exist.' should be 'The directory does not exist.'

Suggested change
/// <exception cref="DirectoryNotFoundException">The directory not exist.</exception>
/// <exception cref="DirectoryNotFoundException">The directory does not exist.</exception>

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options
[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,6 +14,7 @@ public partial class DirectoryServicesProtocolsTests
{
internal static bool LdapConfigurationExists => LdapConfiguration.Configuration != null;
internal static bool IsActiveDirectoryServer => LdapConfigurationExists && LdapConfiguration.Configuration.IsActiveDirectoryServer;
internal static bool UseTls => LdapConfigurationExists && LdapConfiguration.Configuration.UseTls;

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

Expand Down Expand Up @@ -706,6 +705,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(LdapConfigurationExists))]
[PlatformSpecific(TestPlatforms.Linux)]
public void TrustedCertificatesDirectory_ThrowsDirectoryNotFoundException()
{
using (var connection = GetConnection(bind: false))
{
Assert.Throws<DirectoryNotFoundException>(() => connection.SessionOptions.TrustedCertificatesDirectory = "nonexistent");
}
}

[ConditionalFact(nameof(LdapConfigurationExists))]
[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 @@ -786,24 +843,24 @@ private SearchResultEntry SearchUser(LdapConnection connection, string rootDn, s
return null;
}

private LdapConnection GetConnection(string server)
private static LdapConnection GetConnection(string server)
{
LdapDirectoryIdentifier directoryIdentifier = new LdapDirectoryIdentifier(server, fullyQualifiedDnsHostName: true, connectionless: false);

return GetConnection(directoryIdentifier);
}

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

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

Expand All @@ -816,7 +873,11 @@ private static LdapConnection GetConnection(LdapDirectoryIdentifier directoryIde
// 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
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,18 @@ public static bool IsLibLdapInstalled
}
else
{
_isLibLdapInstalled = NativeLibrary.TryLoad("libldap-2.4.so.2", out _);
_isLibLdapInstalled =
NativeLibrary.TryLoad("libldap.so.2", out _) ||
NativeLibrary.TryLoad("libldap-2.6.so.0", out _) ||
NativeLibrary.TryLoad("libldap-2.5.so.0", out _) ||
NativeLibrary.TryLoad("libldap-2.4.so.2", out _);
}
}
return _isLibLdapInstalled.Value;
#else
_isLibLdapInstalled = true; // In .NET Framework ldap is always installed.
return _isLibLdapInstalled.Value;
#endif

return _isLibLdapInstalled.Value;
}
}
}
Expand Down
Loading
Loading