diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs index 6a09c9091103..62478ec578f4 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs @@ -3,13 +3,7 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Net.Security; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; @@ -58,32 +52,11 @@ public async Task BindAsync(EndPoint endPoint, MultiplexedConnectionDe var features = new FeatureCollection(); + // This should always be set in production, but it's not set for InMemory tests. + // The transport will check if the feature is missing. if (listenOptions.HttpsOptions != null) { - // TODO Set other relevant values on options - var sslServerAuthenticationOptions = new SslServerAuthenticationOptions - { - ServerCertificate = listenOptions.HttpsOptions.ServerCertificate, - ApplicationProtocols = new List() { new SslApplicationProtocol("h3"), new SslApplicationProtocol("h3-29") } - }; - - if (listenOptions.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 = listenOptions.HttpsOptions.ServerCertificateSelector(null, host); - if (cert != null) - { - HttpsConnectionMiddleware.EnsureCertificateIsAllowedForServerAuth(cert); - } - return cert!; - }; - } - - features.Set(sslServerAuthenticationOptions); + features.Set(HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions)); } var transport = await _multiplexedTransportFactory.BindAsync(endPoint, features, cancellationToken).ConfigureAwait(false); diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs index edf71145c886..850150ff3919 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs @@ -507,6 +507,47 @@ private static bool IsWindowsVersionIncompatibleWithHttp2() return false; } + + internal static SslServerAuthenticationOptions CreateHttp3Options(HttpsConnectionAdapterOptions httpsOptions) + { + // TODO Set other relevant values on options + var sslServerAuthenticationOptions = new SslServerAuthenticationOptions + { + ServerCertificate = httpsOptions.ServerCertificate, + ApplicationProtocols = new List() { new SslApplicationProtocol("h3"), new SslApplicationProtocol("h3-29") }, + 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); + } + 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 diff --git a/src/Servers/Kestrel/Transport.Quic/test/QuicConnectionContextTests.cs b/src/Servers/Kestrel/Transport.Quic/test/QuicConnectionContextTests.cs index e5939b56b6a1..c5d36252a4b3 100644 --- a/src/Servers/Kestrel/Transport.Quic/test/QuicConnectionContextTests.cs +++ b/src/Servers/Kestrel/Transport.Quic/test/QuicConnectionContextTests.cs @@ -1,17 +1,12 @@ // 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.Buffers; -using System.Collections.Generic; using System.Net.Http; using System.Net.Quic; -using System.Security.Cryptography.X509Certificates; using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; using Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal; using Microsoft.AspNetCore.Testing; @@ -649,46 +644,6 @@ public async Task PersistentState_StreamsReused_StatePersisted() Assert.Equal(true, state); } - [ConditionalFact] - [MsQuicSupported] - [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] - public async Task TlsConnectionFeature_ClientSendsCertificate_PopulatedOnFeature() - { - // Arrange - await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory, clientCertificateRequired: true); - - var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint); - var testCert = TestResources.GetTestCertificate(); - options.ClientAuthenticationOptions.ClientCertificates = new X509CertificateCollection { testCert }; - - // Act - using var quicConnection = new QuicConnection(QuicImplementationProviders.MsQuic, options); - await quicConnection.ConnectAsync().DefaultTimeout(); - - var serverConnection = await connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout(); - // Server waits for stream from client - var serverStreamTask = serverConnection.AcceptAsync().DefaultTimeout(); - - // Client creates stream - using var clientStream = quicConnection.OpenBidirectionalStream(); - await clientStream.WriteAsync(TestData).DefaultTimeout(); - - // Server finishes accepting - var serverStream = await serverStreamTask.DefaultTimeout(); - - // Assert - AssertTlsConnectionFeature(serverConnection.Features, testCert); - AssertTlsConnectionFeature(serverStream.Features, testCert); - - static void AssertTlsConnectionFeature(IFeatureCollection features, X509Certificate2 testCert) - { - var tlsFeature = features.Get(); - Assert.NotNull(tlsFeature); - Assert.NotNull(tlsFeature.ClientCertificate); - Assert.Equal(testCert, tlsFeature.ClientCertificate); - } - } - private record RequestState( QuicConnection QuicConnection, MultiplexedConnectionContext ServerConnection, diff --git a/src/Servers/Kestrel/Transport.Quic/test/QuicConnectionListenerTests.cs b/src/Servers/Kestrel/Transport.Quic/test/QuicConnectionListenerTests.cs index d7e313a613c3..caaa995da72f 100644 --- a/src/Servers/Kestrel/Transport.Quic/test/QuicConnectionListenerTests.cs +++ b/src/Servers/Kestrel/Transport.Quic/test/QuicConnectionListenerTests.cs @@ -2,8 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Net; using System.Net.Quic; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; using Microsoft.AspNetCore.Testing; using Xunit; @@ -13,6 +18,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/35070")] public class QuicConnectionListenerTests : TestApplicationErrorLoggerLoggedTest { + private static readonly byte[] TestData = Encoding.UTF8.GetBytes("Hello world"); + [ConditionalFact] [MsQuicSupported] public async Task AcceptAsync_AfterUnbind_Error() @@ -51,5 +58,66 @@ public async Task AcceptAsync_ClientCreatesConnection_ServerAccepts() // ConnectionClosed isn't triggered because the server initiated close. Assert.False(serverConnection.ConnectionClosed.IsCancellationRequested); } + + [ConditionalFact] + [MsQuicSupported] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] + public async Task ClientCertificate_Required_Sent_Populated() + { + // Arrange + await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory, clientCertificateRequired: true); + + var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint); + var testCert = TestResources.GetTestCertificate(); + options.ClientAuthenticationOptions.ClientCertificates = new X509CertificateCollection { testCert }; + + // Act + using var quicConnection = new QuicConnection(options); + await quicConnection.ConnectAsync().DefaultTimeout(); + + var serverConnection = await connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout(); + // Server waits for stream from client + var serverStreamTask = serverConnection.AcceptAsync().DefaultTimeout(); + + // Client creates stream + using var clientStream = quicConnection.OpenBidirectionalStream(); + await clientStream.WriteAsync(TestData).DefaultTimeout(); + + // Server finishes accepting + var serverStream = await serverStreamTask.DefaultTimeout(); + + // Assert + AssertTlsConnectionFeature(serverConnection.Features, testCert); + AssertTlsConnectionFeature(serverStream.Features, testCert); + + static void AssertTlsConnectionFeature(IFeatureCollection features, X509Certificate2 testCert) + { + var tlsFeature = features.Get(); + Assert.NotNull(tlsFeature); + Assert.NotNull(tlsFeature.ClientCertificate); + Assert.Equal(testCert, tlsFeature.ClientCertificate); + } + } + + [ConditionalFact] + [MsQuicSupported] + // https://github.com/dotnet/runtime/issues/57308, RemoteCertificateValidationCallback should allow us to accept a null cert, + // but it doesn't right now. + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] + public async Task ClientCertificate_Required_NotSent_ConnectionAborted() + { + await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory, clientCertificateRequired: true); + + var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint); + using var clientConnection = new QuicConnection(options); + + var qex = await Assert.ThrowsAsync(async () => await clientConnection.ConnectAsync().DefaultTimeout()); + Assert.Equal("Connection has been shutdown by transport. Error Code: 0x80410100", qex.Message); + + // https://github.com/dotnet/runtime/issues/57246 The accept still completes even though the connection was rejected, but it's already failed. + var serverContext = await connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout(); + qex = await Assert.ThrowsAsync(() => serverContext.ConnectAsync().DefaultTimeout()); + Assert.Equal("Failed to open stream to peer. Error Code: INVALID_STATE", qex.Message); + } } } diff --git a/src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs b/src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs index 730be811833b..da0760e742dd 100644 --- a/src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs +++ b/src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs @@ -49,7 +49,7 @@ public async Task BindAsync_NoServerCertificate_Error() var ex = await Assert.ThrowsAsync(() => quicTransportFactory.BindAsync(new IPEndPoint(0, 0), features: features, cancellationToken: CancellationToken.None).AsTask()).DefaultTimeout(); // Assert - Assert.Equal("SslServerAuthenticationOptions.ServerCertificate must be configured with a value.", ex.Message); + Assert.Equal("SslServerAuthenticationOptions must provide a server certificate using ServerCertificate, ServerCertificateContext, or ServerCertificateSelectionCallback.", ex.Message); } } } diff --git a/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs b/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs index f590dcb9a2c7..ba50c54882c9 100644 --- a/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs +++ b/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs @@ -29,6 +29,8 @@ public static void Main(string[] args) options.ConfigureHttpsDefaults(httpsOptions => { httpsOptions.ServerCertificate = cert; + // httpsOptions.ClientCertificateMode = ClientCertificateMode.AllowCertificate; + // httpsOptions.AllowAnyClientCertificate(); }); options.ListenAnyIP(5000, listenOptions => diff --git a/src/Servers/Kestrel/samples/Http3SampleApp/Startup.cs b/src/Servers/Kestrel/samples/Http3SampleApp/Startup.cs index 367fb530dd4c..1c45e3b34e65 100644 --- a/src/Servers/Kestrel/samples/Http3SampleApp/Startup.cs +++ b/src/Servers/Kestrel/samples/Http3SampleApp/Startup.cs @@ -20,7 +20,7 @@ public void Configure(IApplicationBuilder app) var length = await context.Request.Body.ReadAsync(memory); context.Response.Headers["test"] = "foo"; // for testing - await context.Response.WriteAsync("Hello World! " + context.Request.Protocol); + await context.Response.WriteAsync($"Hello World! {context.Request.Protocol} {context.Connection.ClientCertificate?.Subject}"); }); } } diff --git a/src/Servers/Kestrel/samples/HttpClientApp/HttpClientApp.csproj b/src/Servers/Kestrel/samples/HttpClientApp/HttpClientApp.csproj index 69f7638279de..075dc8ee37dc 100644 --- a/src/Servers/Kestrel/samples/HttpClientApp/HttpClientApp.csproj +++ b/src/Servers/Kestrel/samples/HttpClientApp/HttpClientApp.csproj @@ -9,4 +9,9 @@ + + + + + diff --git a/src/Servers/Kestrel/samples/HttpClientApp/Program.cs b/src/Servers/Kestrel/samples/HttpClientApp/Program.cs index 68b34c3b8529..2ec5588ee732 100644 --- a/src/Servers/Kestrel/samples/HttpClientApp/Program.cs +++ b/src/Servers/Kestrel/samples/HttpClientApp/Program.cs @@ -3,17 +3,27 @@ using System.Net; using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Testing; + +// Console.WriteLine("Ready"); +// Console.ReadKey(); var handler = new SocketsHttpHandler(); handler.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true; +handler.SslOptions.ClientCertificates = new X509CertificateCollection(new[] { TestResources.GetTestCertificate("eku.client.pfx") }); using var client = new HttpClient(handler); -client.DefaultRequestVersion = HttpVersion.Version20; +client.DefaultRequestVersion = + HttpVersion.Version20; + // HttpVersion.Version30; client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; -var response = await client.GetAsync("https://localhost:5001"); +var response = await client.GetAsync("https://localhost:5003"); Console.WriteLine(response); +Console.WriteLine(await response.Content.ReadAsStringAsync()); // Alt-svc enables an upgrade after the first request. -response = await client.GetAsync("https://localhost:5001"); +response = await client.GetAsync("https://localhost:5003"); Console.WriteLine(response); +Console.WriteLine(await response.Content.ReadAsStringAsync()); diff --git a/src/Servers/Kestrel/shared/test/TestResources.cs b/src/Servers/Kestrel/shared/test/TestResources.cs index a11786409095..f4d03eb2501a 100644 --- a/src/Servers/Kestrel/shared/test/TestResources.cs +++ b/src/Servers/Kestrel/shared/test/TestResources.cs @@ -1,11 +1,7 @@ // 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.IO; using System.Security.Cryptography.X509Certificates; -using System.Threading; -using Xunit; namespace Microsoft.AspNetCore.Testing { @@ -25,9 +21,9 @@ public static X509Certificate2 GetTestCertificate(string certName = "testCert.pf { // On Windows, applications should not import PFX files in parallel to avoid a known system-level // race condition bug in native code which can cause crashes/corruption of the certificate state. - if (importPfxMutex != null) + if (importPfxMutex != null && !importPfxMutex.WaitOne(MutexTimeout)) { - Assert.True(importPfxMutex.WaitOne(MutexTimeout), "Cannot acquire the global certificate mutex."); + throw new InvalidOperationException("Cannot acquire the global certificate mutex."); } try diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3Helpers.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3Helpers.cs index 32bc6f619f56..644444b86d36 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3Helpers.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3Helpers.cs @@ -4,11 +4,13 @@ using System.Diagnostics; using System.Net; using System.Net.Http; +using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -17,13 +19,14 @@ namespace Interop.FunctionalTests.Http3 { public static class Http3Helpers { - public static HttpMessageInvoker CreateClient(TimeSpan? idleTimeout = null) + public static HttpMessageInvoker CreateClient(TimeSpan? idleTimeout = null, bool includeClientCert = false) { var handler = new SocketsHttpHandler(); handler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions { RemoteCertificateValidationCallback = (_, __, ___, ____) => true, - TargetHost = "targethost" + TargetHost = "targethost", + ClientCertificates = !includeClientCert ? null : new X509CertificateCollection() { TestResources.GetTestCertificate() }, }; if (idleTimeout != null) { diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs index 234e32cf81b0..1ab070d7d4b9 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs @@ -3,10 +3,12 @@ using System.Net; using System.Net.Http; +using System.Net.Quic; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; +using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Hosting; using Xunit; @@ -52,7 +54,7 @@ public async Task ServerCertificateSelector_Invoked() // https://github.com/dotnet/runtime/issues/57169 Host isn't used for SNI request.Headers.Host = "testhost"; - var response = await client.SendAsync(request, CancellationToken.None); + var response = await client.SendAsync(request, CancellationToken.None).DefaultTimeout(); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadAsStringAsync(); Assert.Equal(HttpVersion.Version30, response.Version); @@ -61,6 +63,171 @@ public async Task ServerCertificateSelector_Invoked() await host.StopAsync().DefaultTimeout(); } + [ConditionalTheory] + [InlineData(ClientCertificateMode.RequireCertificate)] + [InlineData(ClientCertificateMode.AllowCertificate)] + [MsQuicSupported] + public async Task ClientCertificate_AllowOrRequire_Available_Accepted(ClientCertificateMode mode) + { + var builder = CreateHostBuilder(async context => + { + var hasCert = context.Connection.ClientCertificate != null; + await context.Response.WriteAsync(hasCert.ToString()); + }, configureKestrel: kestrelOptions => + { + kestrelOptions.ListenAnyIP(0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http3; + listenOptions.UseHttps(httpsOptions => + { + httpsOptions.ServerCertificate = TestResources.GetTestCertificate(); + httpsOptions.ClientCertificateMode = mode; + httpsOptions.AllowAnyClientCertificate(); + }); + }); + }); + + using var host = builder.Build(); + using var client = Http3Helpers.CreateClient(includeClientCert: true); + + await host.StartAsync().DefaultTimeout(); + + var request = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/"); + request.Version = HttpVersion.Version30; + request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + + var response = await client.SendAsync(request, CancellationToken.None).DefaultTimeout(); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpVersion.Version30, response.Version); + Assert.Equal("True", result); + + await host.StopAsync().DefaultTimeout(); + } + + [ConditionalTheory] + [InlineData(ClientCertificateMode.NoCertificate)] + [InlineData(ClientCertificateMode.DelayCertificate)] + [MsQuicSupported] + public async Task ClientCertificate_NoOrDelayed_Available_Ignored(ClientCertificateMode mode) + { + var builder = CreateHostBuilder(async context => + { + var hasCert = context.Connection.ClientCertificate != null; + await context.Response.WriteAsync(hasCert.ToString()); + }, configureKestrel: kestrelOptions => + { + kestrelOptions.ListenAnyIP(0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http3; + listenOptions.UseHttps(httpsOptions => + { + httpsOptions.ServerCertificate = TestResources.GetTestCertificate(); + httpsOptions.ClientCertificateMode = mode; + httpsOptions.AllowAnyClientCertificate(); + }); + }); + }); + + using var host = builder.Build(); + using var client = Http3Helpers.CreateClient(includeClientCert: true); + + await host.StartAsync().DefaultTimeout(); + + var request = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/"); + request.Version = HttpVersion.Version30; + request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + + var response = await client.SendAsync(request, CancellationToken.None).DefaultTimeout(); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpVersion.Version30, response.Version); + Assert.Equal("False", result); + + await host.StopAsync().DefaultTimeout(); + } + + [ConditionalTheory] + [InlineData(ClientCertificateMode.RequireCertificate)] + [InlineData(ClientCertificateMode.AllowCertificate)] + [MsQuicSupported] + public async Task ClientCertificate_AllowOrRequire_Available_Invalid_Refused(ClientCertificateMode mode) + { + var builder = CreateHostBuilder(async context => + { + var hasCert = context.Connection.ClientCertificate != null; + await context.Response.WriteAsync(hasCert.ToString()); + }, configureKestrel: kestrelOptions => + { + kestrelOptions.ListenAnyIP(0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http3; + listenOptions.UseHttps(httpsOptions => + { + httpsOptions.ServerCertificate = TestResources.GetTestCertificate(); + httpsOptions.ClientCertificateMode = mode; + // httpsOptions.AllowAnyClientCertificate(); // The self-signed cert is invalid. Let it fail the default checks. + }); + }); + }); + + using var host = builder.Build(); + using var client = Http3Helpers.CreateClient(includeClientCert: true); + + await host.StartAsync().DefaultTimeout(); + + var request = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/"); + request.Version = HttpVersion.Version30; + request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + + var ex = await Assert.ThrowsAsync(() => client.SendAsync(request, CancellationToken.None).DefaultTimeout()); + // This poor error is likely a symptom of https://github.com/dotnet/runtime/issues/57246 + // QuicListener returns the connection before (or in spite of) the cert validation failing. + // There's a race where the stream could be accepted before the connection is aborted. + var qex = Assert.IsType(ex.InnerException); + Assert.Equal("Operation aborted.", qex.Message); + + await host.StopAsync().DefaultTimeout(); + } + + [ConditionalFact] + [MsQuicSupported] + public async Task ClientCertificate_Allow_NotAvailable_Optional() + { + var builder = CreateHostBuilder(async context => + { + var hasCert = context.Connection.ClientCertificate != null; + await context.Response.WriteAsync(hasCert.ToString()); + }, configureKestrel: kestrelOptions => + { + kestrelOptions.ListenAnyIP(0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http3; + listenOptions.UseHttps(httpsOptions => + { + httpsOptions.ServerCertificate = TestResources.GetTestCertificate(); + httpsOptions.ClientCertificateMode = ClientCertificateMode.AllowCertificate; + httpsOptions.AllowAnyClientCertificate(); + }); + }); + }); + + using var host = builder.Build(); + using var client = Http3Helpers.CreateClient(includeClientCert: false); + + await host.StartAsync().DefaultTimeout(); + + var request = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/"); + request.Version = HttpVersion.Version30; + request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + + // https://github.com/dotnet/runtime/issues/57308, optional client certs aren't supported. + var ex = await Assert.ThrowsAsync(() => client.SendAsync(request, CancellationToken.None).DefaultTimeout()); + Assert.StartsWith("Connection has been shutdown by transport. Error Code: 0x80410100", ex.Message); + + await host.StopAsync().DefaultTimeout(); + } + private IHostBuilder CreateHostBuilder(RequestDelegate requestDelegate, HttpProtocols? protocol = null, Action configureKestrel = null) { return Http3Helpers.CreateHostBuilder(AddTestLogging, requestDelegate, protocol, configureKestrel);