LocalCertificateSelectionCallback

delegate X509Certificate2? System.Net.Security.LocalCertificateSelectionCallback( object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate2 remoteCertificate, string[] acceptableIssuers );

Represents a method that selects a local X.509 certificate for authentication. This delegate is used with the SslClientAuthenticationOptions.LocalCertificateSelectionCallback property.

Note: This delegate is specifically for client-side certificate selection. For server-side certificate selection, use RemoteCertificateRetrievalCertificateMode and related callbacks.

Parameters

Name Type Description
sender object The object that initiated the certificate selection. This is typically an SslStream instance.
targetHost string The host name of the server to which the client is connecting.
localCertificates X509CertificateCollection A collection of local certificates available for selection.
remoteCertificate X509Certificate2 The certificate presented by the remote server. This parameter can be null if the server did not present a certificate.
acceptableIssuers string[] An array of distinguished names (DNs) of issuers that are acceptable to the remote server. This parameter can be null if the server did not specify acceptable issuers.

Return Value

An X509Certificate2 object representing the selected local certificate, or null if no suitable certificate is found or if no certificate should be sent.

Remarks

The LocalCertificateSelectionCallback delegate is invoked when a client needs to provide a certificate to a server during the SSL/TLS handshake. Your implementation of this delegate should examine the provided parameters and select the most appropriate local certificate from the localCertificates collection.

The callback is particularly useful when multiple client certificates are available or when the client needs to conditionally present a certificate based on the server's requirements (e.g., specified issuers).

If the acceptableIssuers parameter is not null or empty, you should prioritize selecting a certificate whose issuer is present in this array. Otherwise, you can use other criteria such as the certificate's validity period, intended purposes, or simply select the first available certificate.

Example

The following example demonstrates a simple implementation of LocalCertificateSelectionCallback that selects the first available certificate from the provided collection if it's valid and its subject matches the target host.


using System;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

public class CertificateManager
{
    public static X509Certificate2 SelectClientCertificate(
        object sender,
        string targetHost,
        X509CertificateCollection localCertificates,
        X509Certificate2 remoteCertificate,
        string[] acceptableIssuers)
    {
        Console.WriteLine($"Selecting certificate for host: {targetHost}");
        Console.WriteLine($"Available local certificates: {localCertificates.Count}");

        if (localCertificates == null || localCertificates.Count == 0)
        {
            Console.WriteLine("No local certificates available.");
            return null;
        }

        // Prioritize certificates that match acceptable issuers if provided
        if (acceptableIssuers != null && acceptableIssuers.Length > 0)
        {
            Console.WriteLine("Acceptable issuers specified. Searching for matching certificates.");
            foreach (string issuer in acceptableIssuers)
            {
                Console.WriteLine($"  - Acceptable issuer: {issuer}");
            }

            foreach (X509Certificate2 cert in localCertificates)
            {
                if (cert.Issuer.Contains(acceptableIssuers[0])) // Simple check, can be more robust
                {
                    Console.WriteLine($"Found matching certificate by issuer: {cert.Subject}");
                    return cert;
                }
            }
        }

        // If no acceptable issuers or no match, select the first valid certificate
        Console.WriteLine("No matching issuer found or no issuers specified. Selecting the first valid certificate.");
        foreach (X509Certificate2 cert in localCertificates)
        {
            if (IsCertificateValidForClientAuth(cert, targetHost))
            {
                Console.WriteLine($"Selected certificate: {cert.Subject}");
                return cert;
            }
        }

        Console.WriteLine("No suitable certificate found.");
        return null; // No suitable certificate found
    }

    private static bool IsCertificateValidForClientAuth(X509Certificate2 certificate, string targetHost)
    {
        if (certificate == null) return false;

        // Check if the certificate is expired
        if (DateTime.Now > certificate.NotAfter)
        {
            Console.WriteLine($"Certificate expired: {certificate.Subject}");
            return false;
        }

        // Check if the certificate is valid for client authentication
        bool hasClientAuth = false;
        foreach (var extension in certificate.Extensions)
        {
            if (extension.Oid.FriendlyName == "Enhanced Key Usage")
            {
                var ekus = certificate.Extensions.OfType<X509EnhancedKeyUsageExtension>().FirstOrDefault();
                if (ekus != null)
                {
                    foreach (var oid in ekus.EnhancedKeyUsages)
                    {
                        if (oid.Value == "1.3.6.1.5.5.7.3.2") // OID for Client Authentication
                        {
                            hasClientAuth = true;
                            break;
                        }
                    }
                }
            }
            if (hasClientAuth) break;
        }

        if (!hasClientAuth)
        {
            Console.WriteLine($"Certificate does not have Client Authentication EKU: {certificate.Subject}");
            return false;
        }

        // Optional: Check if the certificate's subject name or SAN matches the target host
        // This is a simplified check. Real-world scenarios might need more complex matching.
        if (!string.IsNullOrEmpty(targetHost) && !certificate.Subject.Contains(targetHost) && !certificate.GetNameInfo(X509NameType.DnsName, false).Contains(targetHost))
        {
            // Console.WriteLine($"Certificate subject/SAN does not match target host: {certificate.Subject}");
            // return false;
        }


        Console.WriteLine($"Certificate is valid for client authentication: {certificate.Subject}");
        return true;
    }
}

See Also