// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Net.Security;
using System.Security;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal;

internal sealed class HttpsConnectionMiddleware
{
    private const string EnableWindows81Http2 = "Microsoft.AspNetCore.Server.Kestrel.EnableWindows81Http2";

    private static readonly bool _isWindowsVersionIncompatibleWithHttp2 = IsWindowsVersionIncompatibleWithHttp2();

    private readonly ConnectionDelegate _next;
    private readonly TimeSpan _handshakeTimeout;
    private readonly ILogger<HttpsConnectionMiddleware> _logger;
    private readonly Func<Stream, SslStream> _sslStreamFactory;

    // The following fields are only set by HttpsConnectionAdapterOptions ctor.
    private readonly HttpsConnectionAdapterOptions? _options;
    private readonly KestrelMetrics _metrics;
    private readonly SslStreamCertificateContext? _serverCertificateContext;
    private readonly X509Certificate2? _serverCertificate;
    private readonly Func<ConnectionContext, string?, X509Certificate2?>? _serverCertificateSelector;

    // The following fields are only set by TlsHandshakeCallbackOptions ctor.
    private readonly Func<TlsHandshakeCallbackContext, ValueTask<SslServerAuthenticationOptions>>? _tlsCallbackOptions;
    private readonly object? _tlsCallbackOptionsState;

    // Internal for testing
    internal readonly HttpProtocols _httpProtocols;

    // Pool for cancellation tokens that cancel the handshake
    private readonly CancellationTokenSourcePool _ctsPool = new();

    public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapterOptions options, HttpProtocols httpProtocols, KestrelMetrics metrics)
      : this(next, options, httpProtocols, loggerFactory: NullLoggerFactory.Instance, metrics: metrics)
    {
    }

    public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapterOptions options, HttpProtocols httpProtocols, ILoggerFactory loggerFactory, KestrelMetrics metrics)
    {
        ArgumentNullException.ThrowIfNull(options);

        if (!options.HasServerCertificateOrSelector)
        {
            throw new ArgumentException(CoreStrings.ServerCertificateRequired, nameof(options));
        }

        _next = next;
        _handshakeTimeout = options.HandshakeTimeout;
        _logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
        _metrics = metrics;

        // Something similar to the following could allow us to remove more duplicate logic, but we need https://github.com/dotnet/runtime/issues/40402 to be fixed first.
        //var sniOptionsSelector = new SniOptionsSelector("", new Dictionary<string, SniConfig> { { "*", new SniConfig() } }, new NoopCertificateConfigLoader(), options, options.HttpProtocols, _logger);
        //_httpsOptionsCallback = SniOptionsSelector.OptionsCallback;
        //_httpsOptionsCallbackState = sniOptionsSelector;
        //_sslStreamFactory = s => new SslStream(s);

        _options = options;
        _httpProtocols = ValidateAndNormalizeHttpProtocols(httpProtocols, _logger);

        // capture the certificate now so it can't be switched after validation
        _serverCertificate = options.ServerCertificate;
        _serverCertificateSelector = options.ServerCertificateSelector;

        // If a selector is provided then ignore the cert, it may be a default cert.
        if (_serverCertificateSelector != null)
        {
            // SslStream doesn't allow both.
            _serverCertificate = null;
        }
        else
        {
            Debug.Assert(_serverCertificate != null);

            EnsureCertificateIsAllowedForServerAuth(_serverCertificate, _logger);

            var certificate = _serverCertificate;
            if (!certificate.HasPrivateKey)
            {
                // SslStream historically has logic to deal with certificate missing private keys.
                // By resolving the SslStreamCertificateContext eagerly, we circumvent this logic so
                // try to resolve the certificate from the store if there's no private key in the cert.
                certificate = LocateCertificateWithPrivateKey(certificate);
            }

            // This might be do blocking IO but it'll resolve the certificate chain up front before any connections are
            // made to the server
            _serverCertificateContext = SslStreamCertificateContext.Create(certificate, additionalCertificates: options.ServerCertificateChain);
        }

        var remoteCertificateValidationCallback = _options.ClientCertificateMode == ClientCertificateMode.NoCertificate ?
            (RemoteCertificateValidationCallback?)null : RemoteCertificateValidationCallback;

        _sslStreamFactory = s => new SslStream(s, leaveInnerStreamOpen: false, userCertificateValidationCallback: remoteCertificateValidationCallback);
    }

    internal HttpsConnectionMiddleware(
        ConnectionDelegate next,
        TlsHandshakeCallbackOptions tlsCallbackOptions,
        ILoggerFactory loggerFactory,
        KestrelMetrics metrics)
    {
        _next = next;
        _handshakeTimeout = tlsCallbackOptions.HandshakeTimeout;
        _logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
        _metrics = metrics;

        _tlsCallbackOptions = tlsCallbackOptions.OnConnection;
        _tlsCallbackOptionsState = tlsCallbackOptions.OnConnectionState;
        _httpProtocols = ValidateAndNormalizeHttpProtocols(tlsCallbackOptions.HttpProtocols, _logger);
        _sslStreamFactory = s => new SslStream(s);
    }

    public async Task OnConnectionAsync(ConnectionContext context)
    {
        if (context.Features.Get<ITlsConnectionFeature>() != null)
        {
            await _next(context);
            return;
        }

        var sslDuplexPipe = CreateSslDuplexPipe(
            context.Transport,
            context.Features.Get<IMemoryPoolFeature>()?.MemoryPool ?? MemoryPool<byte>.Shared);
        var sslStream = sslDuplexPipe.Stream;

        var feature = new Core.Internal.TlsConnectionFeature(sslStream, context);
        // Set the mode if options were used. If the callback is used it will set the mode later.
        feature.AllowDelayedClientCertificateNegotation =
            _options?.ClientCertificateMode == ClientCertificateMode.DelayCertificate;
        context.Features.Set<ITlsConnectionFeature>(feature);
        context.Features.Set<ITlsHandshakeFeature>(feature);
        context.Features.Set<ITlsApplicationProtocolFeature>(feature);
        context.Features.Set<ISslStreamFeature>(feature);
        context.Features.Set<SslStream>(sslStream); // Anti-pattern, but retain for back compat

        var metricsContext = context.Features.GetRequiredFeature<IConnectionMetricsContextFeature>().MetricsContext;
        var startTimestamp = Stopwatch.GetTimestamp();
        try
        {
            using var cancellationTokenSource = _ctsPool.Rent();
            cancellationTokenSource.CancelAfter(_handshakeTimeout);

            if (_tlsCallbackOptions is null)
            {
                await DoOptionsBasedHandshakeAsync(context, sslStream, feature, cancellationTokenSource.Token);
            }
            else
            {
                var state = (this, context, feature, metricsContext);
                await sslStream.AuthenticateAsServerAsync(ServerOptionsCallback, state, cancellationTokenSource.Token);
            }
        }
        catch (OperationCanceledException ex)
        {
            RecordHandshakeFailed(_metrics, startTimestamp, Stopwatch.GetTimestamp(), metricsContext, ex);

            _logger.AuthenticationTimedOut();
            await sslStream.DisposeAsync();
            return;
        }
        catch (IOException ex)
        {
            RecordHandshakeFailed(_metrics, startTimestamp, Stopwatch.GetTimestamp(), metricsContext, ex);

            _logger.AuthenticationFailed(ex);
            await sslStream.DisposeAsync();
            return;
        }
        catch (AuthenticationException ex)
        {
            RecordHandshakeFailed(_metrics, startTimestamp, Stopwatch.GetTimestamp(), metricsContext, ex);

            _logger.AuthenticationFailed(ex);

            await sslStream.DisposeAsync();
            return;
        }

        KestrelEventSource.Log.TlsHandshakeStop(context, feature);
        _metrics.TlsHandshakeStop(metricsContext, startTimestamp, Stopwatch.GetTimestamp(), protocol: sslStream.SslProtocol);

        _logger.HttpsConnectionEstablished(context.ConnectionId, sslStream.SslProtocol);

        if (context.Features.Get<IConnectionMetricsTagsFeature>() is { } metricsTags)
        {
            if (KestrelMetrics.TryGetHandshakeProtocol(sslStream.SslProtocol, out var protocolName, out var protocolVersion))
            {
                if (protocolName != "tls")
                {
                    metricsTags.Tags.Add(new KeyValuePair<string, object?>("tls.protocol.name", protocolName));
                }
                metricsTags.Tags.Add(new KeyValuePair<string, object?>("tls.protocol.version", protocolVersion));
            }
        }

        var originalTransport = context.Transport;

        try
        {
            context.Transport = sslDuplexPipe;

            // Disposing the stream will dispose the sslDuplexPipe
            await using (sslStream)
            await using (sslDuplexPipe)
            {
                await _next(context);
                // Dispose the inner stream (SslDuplexPipe) before disposing the SslStream
                // as the duplex pipe can hit an ODE as it still may be writing.
            }
        }
        finally
        {
            // Restore the original so that it gets closed appropriately
            context.Transport = originalTransport;
        }

        static void RecordHandshakeFailed(KestrelMetrics metrics, long startTimestamp, long currentTimestamp, ConnectionMetricsContext metricsContext, Exception ex)
        {
            KestrelEventSource.Log.TlsHandshakeFailed(metricsContext.ConnectionContext.ConnectionId);
            KestrelEventSource.Log.TlsHandshakeStop(metricsContext.ConnectionContext, null);
            metrics.TlsHandshakeStop(metricsContext, startTimestamp, currentTimestamp, exception: ex);
        }
    }

    // This logic is replicated from https://github.com/dotnet/runtime/blob/02b24db7cada5d5806c5cc513e61e44fb2a41944/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs#L195-L262
    // but with public APIs
    private X509Certificate2 LocateCertificateWithPrivateKey(X509Certificate2 certificate)
    {
        Debug.Assert(!certificate.HasPrivateKey, "This should only be called with certificates that don't have a private key");

        _logger.LocatingCertWithPrivateKey(certificate);

        X509Store? OpenStore(StoreLocation storeLocation)
        {
            try
            {
                var store = new X509Store(StoreName.My, storeLocation);
                store.Open(OpenFlags.ReadOnly);
                return store;
            }
            catch (Exception exception)
            {
                if (exception is CryptographicException || exception is SecurityException)
                {
                    _logger.FailedToOpenStore(storeLocation, exception);
                    return null;
                }

                throw;
            }
        }

        try
        {
            var store = OpenStore(StoreLocation.LocalMachine);

            if (store != null)
            {
                using (store)
                {
                    var certs = store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, validOnly: false);

                    if (certs.Count > 0 && certs[0].HasPrivateKey)
                    {
                        _logger.FoundCertWithPrivateKey(certs[0], StoreLocation.LocalMachine);
                        return certs[0];
                    }
                }
            }

            store = OpenStore(StoreLocation.CurrentUser);

            if (store != null)
            {
                using (store)
                {
                    var certs = store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, validOnly: false);

                    if (certs.Count > 0 && certs[0].HasPrivateKey)
                    {
                        _logger.FoundCertWithPrivateKey(certs[0], StoreLocation.CurrentUser);
                        return certs[0];
                    }
                }
            }
        }
        catch (CryptographicException ex)
        {
            // Log as debug since this error is expected an swallowed
            _logger.FailedToFindCertificateInStore(ex);
        }

        // Return the cert, and it will fail later
        return certificate;
    }

    private Task DoOptionsBasedHandshakeAsync(ConnectionContext context, SslStream sslStream, Core.Internal.TlsConnectionFeature feature, CancellationToken cancellationToken)
    {
        Debug.Assert(_options != null, "Middleware must be created with options.");

        // Adapt to the SslStream signature
        ServerCertificateSelectionCallback? selector = null;
        if (_serverCertificateSelector != null)
        {
            selector = (sender, name) =>
            {
                feature.HostName = name ?? string.Empty;
                var cert = _serverCertificateSelector(context, name);
                if (cert != null)
                {
                    EnsureCertificateIsAllowedForServerAuth(cert, _logger);
                }
                return cert!;
            };
        }

        var sslOptions = new SslServerAuthenticationOptions
        {
            ServerCertificate = _serverCertificate,
            ServerCertificateContext = _serverCertificateContext,
            ServerCertificateSelectionCallback = selector,
            ClientCertificateRequired = _options.ClientCertificateMode == ClientCertificateMode.AllowCertificate
                || _options.ClientCertificateMode == ClientCertificateMode.RequireCertificate,
            EnabledSslProtocols = _options.SslProtocols,
            CertificateRevocationCheckMode = _options.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
        };

        ConfigureAlpn(sslOptions, _httpProtocols);

        _options.OnAuthenticate?.Invoke(context, sslOptions);

        KestrelEventSource.Log.TlsHandshakeStart(context, sslOptions);
        _metrics.TlsHandshakeStart(context.Features.GetRequiredFeature<IConnectionMetricsContextFeature>().MetricsContext);

        return sslStream.AuthenticateAsServerAsync(sslOptions, cancellationToken);
    }

    internal static void ConfigureAlpn(SslServerAuthenticationOptions serverOptions, HttpProtocols httpProtocols)
    {
        serverOptions.ApplicationProtocols = new List<SslApplicationProtocol>();

        // This is order sensitive
        if ((httpProtocols & HttpProtocols.Http2) != 0)
        {
            serverOptions.ApplicationProtocols.Add(SslApplicationProtocol.Http2);
            // https://tools.ietf.org/html/rfc7540#section-9.2.1
            serverOptions.AllowRenegotiation = false;
        }

        if ((httpProtocols & HttpProtocols.Http1) != 0)
        {
            serverOptions.ApplicationProtocols.Add(SslApplicationProtocol.Http11);
        }
    }

    internal static bool RemoteCertificateValidationCallback(
        ClientCertificateMode clientCertificateMode,
        Func<X509Certificate2, X509Chain?, SslPolicyErrors, bool>? clientCertificateValidation,
        X509Certificate? certificate,
        X509Chain? chain,
        SslPolicyErrors sslPolicyErrors)
    {
        if (certificate == null)
        {
            return clientCertificateMode != ClientCertificateMode.RequireCertificate;
        }

        if (clientCertificateValidation == null)
        {
            if (sslPolicyErrors != SslPolicyErrors.None)
            {
                return false;
            }
        }

        var certificate2 = ConvertToX509Certificate2(certificate);
        if (certificate2 == null)
        {
            return false;
        }

        if (clientCertificateValidation != null)
        {
            if (!clientCertificateValidation(certificate2, chain, sslPolicyErrors))
            {
                return false;
            }
        }

        return true;
    }

    private bool RemoteCertificateValidationCallback(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors)
    {
        Debug.Assert(_options != null, "Middleware must be created with options.");

        return RemoteCertificateValidationCallback(_options.ClientCertificateMode, _options.ClientCertificateValidation, certificate, chain, sslPolicyErrors);
    }

    private SslDuplexPipe CreateSslDuplexPipe(IDuplexPipe transport, MemoryPool<byte> memoryPool)
    {
        StreamPipeReaderOptions inputPipeOptions = new StreamPipeReaderOptions
        (
            pool: memoryPool,
            bufferSize: memoryPool.GetMinimumSegmentSize(),
            minimumReadSize: memoryPool.GetMinimumAllocSize(),
            leaveOpen: true,
            useZeroByteReads: true
        );

        var outputPipeOptions = new StreamPipeWriterOptions
        (
            pool: memoryPool,
            leaveOpen: true
        );

        return new SslDuplexPipe(transport, inputPipeOptions, outputPipeOptions, _sslStreamFactory);
    }

    private static async ValueTask<SslServerAuthenticationOptions> ServerOptionsCallback(SslStream sslStream, SslClientHelloInfo clientHelloInfo, object? state, CancellationToken cancellationToken)
    {
        var (middleware, context, feature, metricsContext) = (ValueTuple<HttpsConnectionMiddleware, ConnectionContext, Core.Internal.TlsConnectionFeature, ConnectionMetricsContext>)state!;

        feature.HostName = clientHelloInfo.ServerName;
        context.Features.Set(sslStream);
        var callbackContext = new TlsHandshakeCallbackContext
        {
            Connection = context,
            SslStream = sslStream,
            State = middleware._tlsCallbackOptionsState,
            CancellationToken = cancellationToken,
            ClientHelloInfo = clientHelloInfo,
        };
        var sslOptions = await middleware._tlsCallbackOptions!(callbackContext);
        feature.AllowDelayedClientCertificateNegotation = callbackContext.AllowDelayedClientCertificateNegotation;

        // The callback didn't set ALPN so we will.
        if (sslOptions.ApplicationProtocols == null)
        {
            ConfigureAlpn(sslOptions, middleware._httpProtocols);
        }

        KestrelEventSource.Log.TlsHandshakeStart(context, sslOptions);
        middleware._metrics.TlsHandshakeStart(metricsContext);

        return sslOptions;
    }

    internal static void EnsureCertificateIsAllowedForServerAuth(X509Certificate2 certificate, ILogger<HttpsConnectionMiddleware> logger)
    {
        if (!CertificateLoader.IsCertificateAllowedForServerAuth(certificate))
        {
            throw new InvalidOperationException(CoreStrings.FormatInvalidServerCertificateEku(certificate.Thumbprint));
        }
        else if (!CertificateLoader.DoesCertificateHaveASubjectAlternativeName(certificate))
        {
            logger.NoSubjectAlternativeName(certificate.Thumbprint);
        }
    }

    private static X509Certificate2? ConvertToX509Certificate2(X509Certificate? certificate)
    {
        if (certificate == null)
        {
            return null;
        }

        if (certificate is X509Certificate2 cert2)
        {
            return cert2;
        }

        return new X509Certificate2(certificate);
    }

    internal static HttpProtocols ValidateAndNormalizeHttpProtocols(HttpProtocols httpProtocols, ILogger<HttpsConnectionMiddleware> logger)
    {
        // This configuration will always fail per-request, preemptively fail it here. See HttpConnection.SelectProtocol().
        if (httpProtocols == HttpProtocols.Http2)
        {
            if (!TlsAlpn.IsSupported)
            {
                throw new NotSupportedException(CoreStrings.Http2NoTlsAlpn);
            }
            else if (_isWindowsVersionIncompatibleWithHttp2)
            {
                throw new NotSupportedException(CoreStrings.Http2NoTlsWin81);
            }
        }
        else if (httpProtocols == HttpProtocols.Http1AndHttp2 && _isWindowsVersionIncompatibleWithHttp2)
        {
            logger.Http2DefaultCiphersInsufficient();
            return HttpProtocols.Http1;
        }

        return httpProtocols;
    }

    private static bool IsWindowsVersionIncompatibleWithHttp2()
    {
        if (OperatingSystem.IsWindows())
        {
            var enableHttp2OnWindows81 = AppContext.TryGetSwitch(EnableWindows81Http2, out var enabled) && enabled;
            if (Environment.OSVersion.Version < new Version(6, 3) // Missing ALPN support
                                                                  // Win8.1 and 2012 R2 don't support the right cipher configuration by default.
                || (Environment.OSVersion.Version < new Version(10, 0) && !enableHttp2OnWindows81))
            {
                return true;
            }
        }

        return false;
    }

    internal static SslServerAuthenticationOptions CreateHttp3Options(HttpsConnectionAdapterOptions httpsOptions, ILogger<HttpsConnectionMiddleware> logger)
    {
        if (httpsOptions.OnAuthenticate != null)
        {
            throw new NotSupportedException($"The {nameof(HttpsConnectionAdapterOptions.OnAuthenticate)} callback is not supported with HTTP/3.");
        }

        // TODO Set other relevant values on options
        var sslServerAuthenticationOptions = new SslServerAuthenticationOptions
        {
            ServerCertificate = httpsOptions.ServerCertificate,
            ApplicationProtocols = new List<SslApplicationProtocol>() { SslApplicationProtocol.Http3 },
            CertificateRevocationCheckMode = httpsOptions.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
        };

        if (httpsOptions.ServerCertificateSelector != null)
        {
            // We can't set both
            sslServerAuthenticationOptions.ServerCertificate = null;
            sslServerAuthenticationOptions.ServerCertificateSelectionCallback = (sender, host) =>
            {
                // There is no ConnectionContext available durring the QUIC handshake.
                var cert = httpsOptions.ServerCertificateSelector(null, host);
                if (cert != null)
                {
                    EnsureCertificateIsAllowedForServerAuth(cert, logger);
                }
                return cert!;
            };
        }

        // DelayCertificate is prohibited by the HTTP/2 and HTTP/3 protocols, ignore it here.
        if (httpsOptions.ClientCertificateMode == ClientCertificateMode.AllowCertificate
                || httpsOptions.ClientCertificateMode == ClientCertificateMode.RequireCertificate)
        {
            sslServerAuthenticationOptions.ClientCertificateRequired = true; // We have to set this to prompt the client for a cert.
                                                                             // For AllowCertificate we override the missing cert error in RemoteCertificateValidationCallback,
                                                                             // except QuicListener doesn't call the callback for missing certs https://github.com/dotnet/runtime/issues/57308.
            sslServerAuthenticationOptions.RemoteCertificateValidationCallback
                = (object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) =>
                    RemoteCertificateValidationCallback(httpsOptions.ClientCertificateMode, httpsOptions.ClientCertificateValidation, certificate, chain, sslPolicyErrors);
        }

        return sslServerAuthenticationOptions;
    }
}

internal static partial class HttpsConnectionMiddlewareLoggerExtensions
{
    [LoggerMessage(1, LogLevel.Debug, "Failed to authenticate HTTPS connection.", EventName = "AuthenticationFailed")]
    public static partial void AuthenticationFailed(this ILogger<HttpsConnectionMiddleware> logger, Exception exception);

    [LoggerMessage(2, LogLevel.Debug, "Authentication of the HTTPS connection timed out.", EventName = "AuthenticationTimedOut")]
    public static partial void AuthenticationTimedOut(this ILogger<HttpsConnectionMiddleware> logger);

    [LoggerMessage(3, LogLevel.Debug, "Connection {ConnectionId} established using the following protocol: {Protocol}", EventName = "HttpsConnectionEstablished")]
    public static partial void HttpsConnectionEstablished(this ILogger<HttpsConnectionMiddleware> logger, string connectionId, SslProtocols protocol);

    [LoggerMessage(4, LogLevel.Information, "HTTP/2 over TLS is not supported on Windows versions older than Windows 10 and Windows Server 2016 due to incompatible ciphers or missing ALPN support. Falling back to HTTP/1.1 instead.",
        EventName = "Http2DefaultCiphersInsufficient")]
    public static partial void Http2DefaultCiphersInsufficient(this ILogger<HttpsConnectionMiddleware> logger);

    [LoggerMessage(5, LogLevel.Debug, "Searching for certificate with private key and thumbprint {Thumbprint} in the certificate store.", EventName = "LocateCertWithPrivateKey")]
    private static partial void LocatingCertWithPrivateKey(this ILogger<HttpsConnectionMiddleware> logger, string thumbPrint);

    public static void LocatingCertWithPrivateKey(this ILogger<HttpsConnectionMiddleware> logger, X509Certificate2 certificate) => LocatingCertWithPrivateKey(logger, certificate.Thumbprint);

    [LoggerMessage(6, LogLevel.Debug, "Found certificate with private key and thumbprint {Thumbprint} in certificate store {StoreName}.", EventName = "FoundCertWithPrivateKey")]
    public static partial void FoundCertWithPrivateKey(this ILogger<HttpsConnectionMiddleware> logger, string thumbprint, string? storeName);

    public static void FoundCertWithPrivateKey(this ILogger<HttpsConnectionMiddleware> logger, X509Certificate2 certificate, StoreLocation storeLocation)
    {
        var storeLocationString = storeLocation == StoreLocation.LocalMachine ? nameof(StoreLocation.LocalMachine) : nameof(StoreLocation.CurrentUser);
        FoundCertWithPrivateKey(logger, certificate.Thumbprint, storeLocationString);
    }

    [LoggerMessage(7, LogLevel.Debug, "Failure to locate certificate from store.", EventName = "FailToLocateCertificate")]
    public static partial void FailedToFindCertificateInStore(this ILogger<HttpsConnectionMiddleware> logger, Exception exception);

    [LoggerMessage(8, LogLevel.Debug, "Failed to open certificate store {StoreName}.", EventName = "FailToOpenStore")]
    public static partial void FailedToOpenStore(this ILogger<HttpsConnectionMiddleware> logger, string? storeName, Exception exception);

    [LoggerMessage(9, LogLevel.Information, "Certificate with thumbprint {Thumbprint} lacks the subjectAlternativeName (SAN) extension and may not be accepted by browsers.", EventName = "NoSubjectAlternativeName")]
    public static partial void NoSubjectAlternativeName(this ILogger<HttpsConnectionMiddleware> logger, string thumbprint);

    public static void FailedToOpenStore(this ILogger<HttpsConnectionMiddleware> logger, StoreLocation storeLocation, Exception exception)
    {
        var storeLocationString = storeLocation == StoreLocation.LocalMachine ? nameof(StoreLocation.LocalMachine) : nameof(StoreLocation.CurrentUser);
        FailedToOpenStore(logger, storeLocationString, exception);
    }
}
