Skip to content
Draft
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 @@ -159,6 +159,8 @@ internal enum LdapOption
LDAP_OPT_NETWORK_TIMEOUT = 0x5005, // Not Supported in Windows
LDAP_OPT_URI = 0x5006, // Not Supported in Windows
LDAP_OPT_X_TLS_CACERTDIR = 0x6003, // Not Supported in Windows
LDAP_OPT_X_TLS_CONNECT_CB = 0x600c, // Not Supported in Windows
LDAP_OPT_X_TLS_CONNECT_ARG = 0x600d, // 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,
Expand Down
12 changes: 9 additions & 3 deletions src/libraries/Common/src/Interop/Linux/OpenLdap/Interop.Ldap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ internal enum SaslChallengeType

internal delegate int LDAP_SASL_INTERACT_PROC(IntPtr ld, uint flags, IntPtr defaults, IntPtr interact);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate int LDAP_TLS_CONNECT_CB(IntPtr ld, IntPtr ssl, IntPtr ctx, IntPtr arg);

internal static partial class Interop
{
public const string LDAP_SASL_SIMPLE = null;
Expand Down Expand Up @@ -156,15 +159,18 @@ public static partial int ldap_search(
[LibraryImport(Libraries.OpenLdap, EntryPoint = "ldap_set_option")]
public static partial int ldap_set_option_clientcert(ConnectionHandle ldapHandle, LdapOption option, QUERYCLIENTCERT outValue);

[LibraryImport(Libraries.OpenLdap, EntryPoint = "ldap_set_option")]
public static partial int ldap_set_option_servercert(ConnectionHandle ldapHandle, LdapOption option, VERIFYSERVERCERT outValue);

[LibraryImport(Libraries.OpenLdap, EntryPoint = "ldap_set_option", SetLastError = true)]
public static partial int ldap_set_option_int(ConnectionHandle ld, LdapOption option, ref int inValue);

[LibraryImport(Libraries.OpenLdap, EntryPoint = "ldap_set_option")]
public static partial int ldap_set_option_ptr(ConnectionHandle ldapHandle, LdapOption option, ref IntPtr inValue);

//[LibraryImport(Libraries.OpenLdap, EntryPoint = "ldap_set_option")]
//public static partial int ldap_set_option_ptr(IntPtr ldapHandle, LdapOption option, ref IntPtr inValue);

[LibraryImport(Libraries.OpenLdap, EntryPoint = "ldap_set_option")]
internal static partial int ldap_set_option_ptr_value(ConnectionHandle ldapHandle, LdapOption option, IntPtr inValue);

[LibraryImport(Libraries.OpenLdap, EntryPoint = "ldap_set_option")]
public static partial int ldap_set_option_string(ConnectionHandle ldapHandle, LdapOption option, [MarshalAs(UnmanagedType.LPUTF8Str)] string inValue);

Expand Down
53 changes: 53 additions & 0 deletions src/libraries/Common/src/Interop/Linux/OpenSsl/Interop.OpenSsl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Runtime.InteropServices;

internal static partial class Interop
{
internal static partial class OpenSsl
{
// Targeting OpenSSL 3
private const string LibSsl = "libssl.so.3";
private const string LibCrypto = "libcrypto.so.3";
Comment on lines +12 to +13
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

Interop.OpenSsl hardcodes libssl.so.3/libcrypto.so.3, which will fail on supported distros that still ship OpenSSL 1.1 (and it also won’t work on macOS). Please switch to a version-resilient loading strategy consistent with the repo (e.g., route through the existing OpenSSL shim in System.Security.Cryptography.Native, or introduce a shim export for the needed APIs) rather than binding directly to versioned .so names.

Suggested change
private const string LibSsl = "libssl.so.3";
private const string LibCrypto = "libcrypto.so.3";
private const string LibSsl = "libssl";
private const string LibCrypto = "libcrypto";

Copilot uses AI. Check for mistakes.

internal const int SSL_VERIFY_NONE = 0x00;
internal const int SSL_VERIFY_PEER = 0x01;

// OpenSSL: int (*callback)(int preverify_ok, X509_STORE_CTX *x509_ctx);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate int VerifyCallback(int preverify_ok, IntPtr x509StoreCtx);

[LibraryImport(LibSsl, EntryPoint = "SSL_set_verify")]
internal static partial void SSL_set_verify(
IntPtr ssl,
int mode,
VerifyCallback callback);

// Reserved for future use
[LibraryImport(LibSsl, EntryPoint = "SSL_CTX_set_verify")]
internal static partial void SSL_CTX_set_verify(
IntPtr ctx,
int mode,
VerifyCallback callback);

[LibraryImport(LibCrypto, EntryPoint = "X509_STORE_CTX_get_current_cert")]
internal static partial IntPtr X509_STORE_CTX_get_current_cert(IntPtr ctx);

[LibraryImport(LibCrypto, EntryPoint = "X509_STORE_CTX_get_error_depth")]
internal static partial int X509_STORE_CTX_get_error_depth(IntPtr ctx);

// Reserved for future use
[LibraryImport(LibCrypto, EntryPoint = "X509_STORE_CTX_get_error")]
internal static partial int X509_STORE_CTX_get_error(IntPtr ctx);

// int i2d_X509(X509 *a, unsigned char **out);
[LibraryImport(LibCrypto, EntryPoint = "i2d_X509")]
internal static partial int i2d_X509(IntPtr x509, ref IntPtr pp);

// void CRYPTO_free(void *ptr, const char *file, int line);
[LibraryImport(LibCrypto, EntryPoint = "CRYPTO_free")]
internal static partial void CRYPTO_free(IntPtr ptr, IntPtr file, int line);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate Interop.BOOL VERIFYSERVERCERT(IntPtr Connection, IntPtr pServerCert);

internal static partial class Interop
{
internal static partial class Ldap
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
<Compile Include="$(CommonPath)System\LocalAppContextSwitches.Common.cs" Link="Common\System\LocalAppContextSwitches.Common.cs" />
<Compile Include="$(CommonPath)Interop\Linux\OpenLdap\Interop.Ldap.cs" Link="Common\Interop\Linux\OpenLdap\Interop.Ldap.cs" />
<Compile Include="$(CommonPath)Interop\Linux\OpenLdap\Interop.Ber.cs" Link="Common\Interop\Linux\OpenLdap\Interop.Ber.cs" />
<Compile Include="$(CommonPath)Interop\Linux\OpenSsl\Interop.OpenSsl.cs" Link="Common\Interop\Linux\OpenSsl\Interop.OpenSsl.cs" />
</ItemGroup>
Comment on lines 85 to 89
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

This new OpenSSL interop file is added to the linux or osx compile ItemGroup, but it P/Invokes Linux .so names. That means setting VerifyServerCertificate on macOS will likely throw DllNotFoundException. If the intent is Linux-only support, restrict inclusion to Linux; otherwise add an OS X-specific interop (or a shared shim-based approach) that resolves the correct library names.

Copilot uses AI. Check for mistakes.

<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'linux'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,11 @@ internal static int SearchDirectory(ConnectionHandle ldapHandle, string dn, int

internal static int SetTimevalOption(ConnectionHandle ldapHandle, LdapOption option, ref LDAP_TIMEVAL inValue) => Interop.Ldap.ldap_set_option_timeval(ldapHandle, option, ref inValue);

// This option is not supported in Linux, so it would most likely throw.
internal static int SetServerCertOption(ConnectionHandle ldapHandle, LdapOption option, VERIFYSERVERCERT outValue) => Interop.Ldap.ldap_set_option_servercert(ldapHandle, option, outValue);
internal static int SetServerCertOption(ConnectionHandle ldapHandle, LdapOption option, IntPtr inValue)
{
IntPtr functionPointer = inValue;
return Interop.Ldap.ldap_set_option_ptr(ldapHandle, option, ref functionPointer);
}
Comment on lines +120 to +124
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

LdapPal.SetServerCertOption now wraps ldap_set_option_ptr with a ref IntPtr, which passes a pointer-to-pointer. For options like LDAP_OPT_X_TLS_CONNECT_CB (function pointer value), the call sites in this PR use a different P/Invoke that passes the pointer value directly. To prevent future misuse, either remove SetServerCertOption on Linux if it’s unused, or implement it in terms of the correct setter (ldap_set_option_ptr_value) for callback-style options.

Suggested change
internal static int SetServerCertOption(ConnectionHandle ldapHandle, LdapOption option, IntPtr inValue)
{
IntPtr functionPointer = inValue;
return Interop.Ldap.ldap_set_option_ptr(ldapHandle, option, ref functionPointer);
}
internal static int SetServerCertOption(ConnectionHandle ldapHandle, LdapOption option, IntPtr inValue) =>
Interop.Ldap.ldap_set_option_ptr_value(ldapHandle, option, inValue);

Copilot uses AI. Check for mistakes.

internal static unsafe int BindToDirectory(ConnectionHandle ld, string who, string passwd)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,105 @@

using System.ComponentModel;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

namespace System.DirectoryServices.Protocols
{
public partial class LdapSessionOptions
{
private int _verifyCallbackInvoked;
private int _verifyCallbackResult;
private LDAP_TLS_CONNECT_CB _serverCertificateRoutine;
private Interop.OpenSsl.VerifyCallback? _openSslVerifyRoutine;

private void InitializeServerCertificateDelegate()
{
_verifyCallbackInvoked = 0;
_verifyCallbackResult = 0;
_serverCertificateRoutine = new LDAP_TLS_CONNECT_CB(SetOpenSslCallback);
_openSslVerifyRoutine ??= new Interop.OpenSsl.VerifyCallback(ProcessServerCertificate);
}
Comment on lines +19 to +25
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

_verifyCallbackInvoked/_verifyCallbackResult are only initialized in the constructor. If the same LdapConnection performs more than one TLS handshake over its lifetime (reconnect, StartTLS again, etc.), the callback result from the first handshake will be reused and the user delegate won’t run again. Consider resetting this per-handshake (e.g., when SetOpenSslCallback is invoked) rather than per LdapSessionOptions instance.

Copilot uses AI. Check for mistakes.

static partial void PALCertFreeCRLContext(IntPtr certPtr);

private int SetOpenSslCallback(IntPtr ld, IntPtr ssl, IntPtr ctx, IntPtr arg)
{
Interop.OpenSsl.SSL_set_verify(ssl, Interop.OpenSsl.SSL_VERIFY_PEER, _openSslVerifyRoutine);

return 1; // continue the handshake
}

private int ProcessServerCertificate(int preverify_ok, IntPtr x509StoreCtx)
{
if (_serverCertificateDelegate == null)
{
return preverify_ok;
}

int depth = Interop.OpenSsl.X509_STORE_CTX_get_error_depth(x509StoreCtx);
if (depth != 0)
{
return 1;
}

// The callback is only expected to be invoked once per connection, but if it is invoked multiple times, the result from the first invocation will be returned.
// This emulates Windows behavior.
if (System.Threading.Interlocked.CompareExchange(ref _verifyCallbackInvoked, 1, 0) != 0)
{
return System.Threading.Volatile.Read(ref _verifyCallbackResult);
}

int result = 0;
X509Certificate? cert = TryGetX509Certificate2FromStoreCtx(x509StoreCtx);
if (cert == null)
{
System.Threading.Volatile.Write(ref _verifyCallbackResult, 0);
return 0;
}

try
{
result = _serverCertificateDelegate(_connection, cert) ? 1 : 0;
return result;
}
catch
{
result = 0;
return 0;
}
finally
{
cert.Dispose();
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

cert.Dispose() in the verify callback can leave user code holding a disposed X509Certificate instance if they capture it from the callback for later inspection/logging. Windows implementation doesn’t dispose the certificate passed to the delegate. To keep behavior consistent and avoid surprising ObjectDisposedExceptions, don’t dispose the certificate here (or pass a non-disposable clone).

Suggested change
cert.Dispose();

Copilot uses AI. Check for mistakes.
System.Threading.Volatile.Write(ref _verifyCallbackResult, result);
}
}

private static X509Certificate2? TryGetX509Certificate2FromStoreCtx(IntPtr x509StoreCtx)
{
IntPtr x509 = Interop.OpenSsl.X509_STORE_CTX_get_current_cert(x509StoreCtx);
if (x509 == IntPtr.Zero)
return null;

// OpenSSL will allocate a buffer and write its address into pp if pp starts as NULL.
IntPtr pDer = IntPtr.Zero;
int len = Interop.OpenSsl.i2d_X509(x509, ref pDer);
if (len <= 0 || pDer == IntPtr.Zero)
return null;

try
{
byte[] der = new byte[len];
Marshal.Copy(pDer, der, 0, len);
return X509CertificateLoader.LoadCertificate(der);
}
finally
{
Interop.OpenSsl.CRYPTO_free(pDer, IntPtr.Zero, 0);
}
}

private bool _secureSocketLayer;

/// <summary>
Expand Down Expand Up @@ -76,6 +167,36 @@ public ReferralChasingOptions ReferralChasing
}
}

public VerifyServerCertificateCallback? VerifyServerCertificate
{
get
{
if (_connection._disposed)
{
throw new ObjectDisposedException(GetType().Name);
}

return _serverCertificateDelegate;
}
set
{
if (_connection._disposed)
{
throw new ObjectDisposedException(GetType().Name);
}

if (value != null)
{
IntPtr functionPointer = Marshal.GetFunctionPointerForDelegate(_serverCertificateRoutine);
int error = Interop.Ldap.ldap_set_option_ptr_value(_connection._ldapHandle, LdapOption.LDAP_OPT_X_TLS_CONNECT_CB, functionPointer);

ErrorChecking.CheckAndSetLdapError(error);
}

Comment on lines +188 to +195
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

With Linux support added, VerifyServerCertificate is no longer Windows-only behavior, but existing tests currently validate set/get only on Windows. Please extend LdapSessionOptionsTests.VerifyServerCertificate_Set_GetReturnsExpected to run on Linux as well (and ideally validate that returning false aborts the handshake, if test infrastructure allows).

Suggested change
if (value != null)
{
IntPtr functionPointer = Marshal.GetFunctionPointerForDelegate(_serverCertificateRoutine);
int error = Interop.Ldap.ldap_set_option_ptr_value(_connection._ldapHandle, LdapOption.LDAP_OPT_X_TLS_CONNECT_CB, functionPointer);
ErrorChecking.CheckAndSetLdapError(error);
}
IntPtr functionPointer = value is not null
? Marshal.GetFunctionPointerForDelegate(_serverCertificateRoutine)
: IntPtr.Zero;
int error = Interop.Ldap.ldap_set_option_ptr_value(
_connection._ldapHandle,
LdapOption.LDAP_OPT_X_TLS_CONNECT_CB,
functionPointer);
ErrorChecking.CheckAndSetLdapError(error);

Copilot uses AI. Check for mistakes.
_serverCertificateDelegate = value;
}
}

/// <summary>
/// Create a new TLS library context.
/// Calling this is necessary after setting TLS-based options, such as <c>TrustedCertificatesDirectory</c>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,48 @@

using System.ComponentModel;
using System.Runtime.Versioning;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;

namespace System.DirectoryServices.Protocols
{
public partial class LdapSessionOptions
{
private VERIFYSERVERCERT _serverCertificateRoutine;

private void InitializeServerCertificateDelegate()
{
_serverCertificateRoutine = new VERIFYSERVERCERT(ProcessServerCertificate);
}

private static void PALCertFreeCRLContext(IntPtr certPtr) => Interop.Ldap.CertFreeCRLContext(certPtr);

private Interop.BOOL ProcessServerCertificate(IntPtr connection, IntPtr serverCert)
{
// If callback is not specified by user, it means the server certificate is accepted.
bool value = true;
if (_serverCertificateDelegate != null)
{
IntPtr certPtr = IntPtr.Zero;
X509Certificate certificate = null;
try
{
Debug.Assert(serverCert != IntPtr.Zero);
certPtr = Marshal.ReadIntPtr(serverCert);
certificate = new X509Certificate(certPtr);
}
finally
{
PALCertFreeCRLContext(certPtr);
}

value = _serverCertificateDelegate(_connection, certificate);
}

return value ? Interop.BOOL.TRUE : Interop.BOOL.FALSE;
}

[UnsupportedOSPlatform("windows")]
public string TrustedCertificatesDirectory
{
Expand Down Expand Up @@ -52,6 +87,34 @@ public ReferralChasingOptions ReferralChasing
}
}

public VerifyServerCertificateCallback VerifyServerCertificate
{
get
{
if (_connection._disposed)
{
throw new ObjectDisposedException(GetType().Name);
}

return _serverCertificateDelegate;
}
set
{
if (_connection._disposed)
{
throw new ObjectDisposedException(GetType().Name);
}

if (value != null)
{
int error = LdapPal.SetServerCertOption(_connection._ldapHandle, LdapOption.LDAP_OPT_SERVER_CERTIFICATE, _serverCertificateRoutine);
ErrorChecking.CheckAndSetLdapError(error);
}

_serverCertificateDelegate = value;
}
}

// In practice, this apparently rarely if ever contains useful text
internal string ServerErrorMessage => GetStringValueHelper(LdapOption.LDAP_OPT_SERVER_ERROR, true);
}
Expand Down
Loading
Loading