File: ClientCertificateValidator.cs
Web Access
Project: src\aspnetcore\src\Servers\Kestrel\Transport.DirectTls\src\Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls.csproj (Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
 
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls;
 
/// <summary>
/// Runs the endpoint's client-certificate validation for a completed handshake on the Linux fd fast path.
/// The runtime cannot enforce the accept/reject decision on that path (OpenSSL only applies SSL_VERIFY_PEER,
/// not FAIL_IF_NO_PEER_CERT, and the fd read/write fast paths bypass the runtime's pending-validation fault),
/// so the transport builds the peer's chain and invokes the endpoint's callback itself, exactly as
/// <see cref="System.Net.Security.SslStream"/> does for server-side client-certificate validation.
/// This logic is factored out of the native pump loop so it can be unit tested without epoll or a live TLS session.
/// </summary>
internal static class ClientCertificateValidator
{
    /// <summary>
    /// Builds the <see cref="X509Chain"/> used to validate a peer's client certificate, mirroring
    /// <see cref="SslStream"/>'s default server-side client-certificate validation policy.
    /// </summary>
    /// <remarks>
    /// The returned chain is configured for use on the pump thread:
    /// <list type="bullet">
    /// <item>
    /// <see cref="X509RevocationMode.NoCheck"/> avoids blocking the pump thread on CRL/OCSP network I/O and
    /// matches the transport default (<c>CheckCertificateRevocation == false</c>).
    /// </item>
    /// <item>
    /// <see cref="X509ChainPolicy.DisableCertificateDownloads"/> is <see langword="true"/> so the chain
    /// engine never makes synchronous AIA fetches for missing intermediates. <see cref="X509Chain.Build"/>
    /// runs on the pump thread, so an attacker-supplied leaf whose Authority Information Access extension
    /// points at an unreachable or slow URL could otherwise stall every connection this pump owns.
    /// <see cref="SslStream"/> sets the same flag on the server side for this reason.
    /// Legitimate clients send their intermediates in the handshake, which are supplied here via <paramref name="intermediates"/>.
    /// </item>
    /// </list>
    /// The caller owns the returned chain and must dispose it.
    /// </remarks>
    /// <param name="intermediates">
    /// Intermediate certificates the peer sent during the handshake, added to the chain's extra store so a
    /// valid chain can be built without any network fetch. May be <see langword="null"/>.
    /// </param>
    internal static X509Chain BuildChain(X509Certificate2Collection? intermediates)
    {
        var chain = new X509Chain();
        chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
        chain.ChainPolicy.DisableCertificateDownloads = true;
        if (intermediates is not null)
        {
            chain.ChainPolicy.ExtraStore.AddRange(intermediates);
        }
 
        return chain;
    }
 
    /// <summary>
    /// Runs the endpoint's client-certificate validation callback and returns whether the connection is
    /// accepted.
    /// </summary>
    /// <param name="sender">The sender passed to the callback (the peer's TLS session).</param>
    /// <param name="presentedCertificate">
    /// The peer's leaf certificate, or <see langword="null"/> when the client presented none.
    /// </param>
    /// <param name="intermediates">
    /// Intermediate certificates the peer sent, used to build the chain. Ignored when
    /// <paramref name="presentedCertificate"/> is <see langword="null"/>.
    /// </param>
    /// <param name="validateClientCertificate">The endpoint's validation callback. Must not be null.</param>
    /// <returns><see langword="true"/> to accept the connection; <see langword="false"/> to reject it.</returns>
    internal static bool Validate(
        object sender,
        X509Certificate2? presentedCertificate,
        X509Certificate2Collection? intermediates,
        RemoteCertificateValidationCallback validateClientCertificate)
    {
        if (presentedCertificate is null)
        {
            // No client certificate presented. The endpoint's callback encodes the mode:
            // AllowCertificate accepts (returns true); RequireCertificate rejects (returns false).
            return validateClientCertificate(sender, null, null, SslPolicyErrors.RemoteCertificateNotAvailable);
        }
 
        // Build the peer's chain so the endpoint's callback and any operator ClientCertificateValidation
        // delegate observe a real chain and errors (mirrors SslStream's default client-certificate
        // validation). The chain is untrusted unless the issuing CA is in the machine store; the callback
        // decides whether to accept despite RemoteCertificateChainErrors.
        using var chain = BuildChain(intermediates);
        var sslPolicyErrors = chain.Build(presentedCertificate)
            ? SslPolicyErrors.None
            : SslPolicyErrors.RemoteCertificateChainErrors;
 
        return validateClientCertificate(sender, presentedCertificate, chain, sslPolicyErrors);
    }
}