From a671f9652808921d6bbe74994c16065372bec6f6 Mon Sep 17 00:00:00 2001 From: Chris Ross <chrross@microsoft.com> Date: Tue, 10 Aug 2021 16:56:21 -0700 Subject: [PATCH] Enable ServerCertificateSelector for HTTP/3 #34858 (#35243) --- .../Core/src/HttpsConnectionAdapterOptions.cs | 4 +- .../Infrastructure/TransportManager.cs | 17 ++++ .../Kestrel/Core/src/PublicAPI.Unshipped.txt | 2 +- .../src/QuicTransportFactory.cs | 7 +- .../Kestrel/samples/Http3SampleApp/Program.cs | 35 +++++++- .../Http3/Http3Helpers.cs | 77 ++++++++++++++++ .../Http3/Http3RequestTests.cs | 88 ++++--------------- .../Http3/Http3TlsTests.cs | 69 +++++++++++++++ 8 files changed, 221 insertions(+), 78 deletions(-) create mode 100644 src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3Helpers.cs create mode 100644 src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs diff --git a/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs b/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs index 5d82b34e42c..e18238e8ce7 100644 --- a/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs +++ b/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs @@ -42,13 +42,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https /// <summary> /// <para> /// A callback that will be invoked to dynamically select a server certificate. This is higher priority than ServerCertificate. - /// If SNI is not available then the name parameter will be null. + /// If SNI is not available then the name parameter will be null. The <see cref="ConnectionContext"/> will be null for HTTP/3 connections. /// </para> /// <para> /// If the server certificate has an Extended Key Usage extension, the usages must include Server Authentication (OID 1.3.6.1.5.5.7.3.1). /// </para> /// </summary> - public Func<ConnectionContext, string?, X509Certificate2?>? ServerCertificateSelector { get; set; } + public Func<ConnectionContext?, string?, X509Certificate2?>? ServerCertificateSelector { get; set; } /// <summary> /// Specifies the client certificate requirements for a HTTPS connection. Defaults to <see cref="ClientCertificateMode.NoCertificate"/>. diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs index 392dd7b533f..6a09c909110 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure { @@ -66,6 +67,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure ApplicationProtocols = new List<SslApplicationProtocol>() { 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); } diff --git a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt index ab9bdb1830c..9bbfeb01dde 100644 --- a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt @@ -183,7 +183,7 @@ Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions.OnAuthen Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions.OnAuthenticate.set -> void Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions.ServerCertificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2? Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions.ServerCertificate.set -> void -Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions.ServerCertificateSelector.get -> System.Func<Microsoft.AspNetCore.Connections.ConnectionContext!, string?, System.Security.Cryptography.X509Certificates.X509Certificate2?>? +Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions.ServerCertificateSelector.get -> System.Func<Microsoft.AspNetCore.Connections.ConnectionContext?, string?, System.Security.Cryptography.X509Certificates.X509Certificate2?>? Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions.ServerCertificateSelector.set -> void Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader.AnyIPEndpoint(int port) -> Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader! Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader.AnyIPEndpoint(int port, System.Action<Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions!>! configure) -> Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader! diff --git a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs index db03c9a308b..8a82ae301f9 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs @@ -54,9 +54,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic { throw new InvalidOperationException("Couldn't find HTTPS configuration for QUIC transport."); } - if (sslServerAuthenticationOptions.ServerCertificate == null) + if (sslServerAuthenticationOptions.ServerCertificate == null + && sslServerAuthenticationOptions.ServerCertificateContext == null + && sslServerAuthenticationOptions.ServerCertificateSelectionCallback == null) { - var message = $"{nameof(SslServerAuthenticationOptions)}.{nameof(SslServerAuthenticationOptions.ServerCertificate)} must be configured with a value."; + var message = $"{nameof(SslServerAuthenticationOptions)} must provide a server certificate using {nameof(SslServerAuthenticationOptions.ServerCertificate)}," + + $" {nameof(SslServerAuthenticationOptions.ServerCertificateContext)}, or {nameof(SslServerAuthenticationOptions.ServerCertificateSelectionCallback)}."; throw new InvalidOperationException(message); } diff --git a/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs b/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs index f66f6a01033..f590dcb9a2c 100644 --- a/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs +++ b/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs @@ -25,6 +25,7 @@ namespace Http3SampleApp .ConfigureKestrel((context, options) => { var cert = CertificateLoader.LoadFromStoreCert("localhost", StoreName.My.ToString(), StoreLocation.CurrentUser, false); + options.ConfigureHttpsDefaults(httpsOptions => { httpsOptions.ServerCertificate = cert; @@ -54,13 +55,41 @@ namespace Http3SampleApp { listenOptions.UseHttps(httpsOptions => { - httpsOptions.ServerCertificateSelector = (_, _) => cert; + // ConnectionContext is null + httpsOptions.ServerCertificateSelector = (context, host) => cert; }); listenOptions.UseConnectionLogging(); - listenOptions.Protocols = HttpProtocols.Http1AndHttp2; // TODO: http3 + listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3; }); + // No SslServerAuthenticationOptions callback is currently supported by QuicListener options.ListenAnyIP(5004, listenOptions => + { + listenOptions.UseHttps(httpsOptions => + { + httpsOptions.OnAuthenticate = (_, sslOptions) => sslOptions.ServerCertificate = cert; + }); + listenOptions.UseConnectionLogging(); + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); + + // ServerOptionsSelectionCallback isn't currently supported by QuicListener + options.ListenAnyIP(5005, listenOptions => + { + ServerOptionsSelectionCallback callback = (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) => + { + var options = new SslServerAuthenticationOptions() + { + ServerCertificate = cert, + }; + return new ValueTask<SslServerAuthenticationOptions>(options); + }; + listenOptions.UseHttps(callback, state: null); + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); + + // TlsHandshakeCallbackOptions (ServerOptionsSelectionCallback) isn't currently supported by QuicListener + options.ListenAnyIP(5006, listenOptions => { listenOptions.UseHttps(new TlsHandshakeCallbackOptions() { @@ -74,7 +103,7 @@ namespace Http3SampleApp }, }); listenOptions.UseConnectionLogging(); - listenOptions.Protocols = HttpProtocols.Http1AndHttp2; // TODO: http3 + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; }); }) .UseStartup<Startup>(); diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3Helpers.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3Helpers.cs new file mode 100644 index 00000000000..32bc6f619f5 --- /dev/null +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3Helpers.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Net; +using System.Net.Http; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Interop.FunctionalTests.Http3 +{ + public static class Http3Helpers + { + public static HttpMessageInvoker CreateClient(TimeSpan? idleTimeout = null) + { + var handler = new SocketsHttpHandler(); + handler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions + { + RemoteCertificateValidationCallback = (_, __, ___, ____) => true, + TargetHost = "targethost" + }; + if (idleTimeout != null) + { + handler.PooledConnectionIdleTimeout = idleTimeout.Value; + } + + return new HttpMessageInvoker(handler); + } + + public static IHostBuilder CreateHostBuilder(Action<IServiceCollection> configureServices, RequestDelegate requestDelegate, HttpProtocols? protocol = null, Action<KestrelServerOptions> configureKestrel = null) + { + return new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseKestrel(o => + { + if (configureKestrel == null) + { + o.Listen(IPAddress.Parse("127.0.0.1"), 0, listenOptions => + { + listenOptions.Protocols = protocol ?? HttpProtocols.Http3; + listenOptions.UseHttps(); + }); + } + else + { + configureKestrel(o); + } + }) + .Configure(app => + { + app.Run(requestDelegate); + }); + }) + .ConfigureServices(configureServices) + .ConfigureHostOptions(o => + { + if (Debugger.IsAttached) + { + // Avoid timeout while debugging. + o.ShutdownTimeout = TimeSpan.FromHours(1); + } + else + { + o.ShutdownTimeout = TimeSpan.FromSeconds(1); + } + }); + } + } +} diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs index c5adad76592..81d705f5f59 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs @@ -6,7 +6,6 @@ using System.Net; using System.Net.Http; using System.Net.Quic; using System.Text; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting; @@ -91,7 +90,7 @@ namespace Interop.FunctionalTests.Http3 }, protocol: protocol); using (var host = builder.Build()) - using (var client = CreateClient()) + using (var client = Http3Helpers.CreateClient()) { await host.StartAsync().DefaultTimeout(); @@ -193,7 +192,7 @@ namespace Interop.FunctionalTests.Http3 }, protocol: protocol); using (var host = builder.Build()) - using (var client = CreateClient()) + using (var client = Http3Helpers.CreateClient()) { await host.StartAsync(); @@ -232,7 +231,7 @@ namespace Interop.FunctionalTests.Http3 }, protocol: protocol); using (var host = builder.Build()) - using (var client = CreateClient()) + using (var client = Http3Helpers.CreateClient()) { await host.StartAsync(); @@ -276,7 +275,7 @@ namespace Interop.FunctionalTests.Http3 }); using (var host = builder.Build()) - using (var client = CreateClient()) + using (var client = Http3Helpers.CreateClient()) { await host.StartAsync(); @@ -344,7 +343,7 @@ namespace Interop.FunctionalTests.Http3 }, protocol: protocol); using (var host = builder.Build()) - using (var client = CreateClient()) + using (var client = Http3Helpers.CreateClient()) { await host.StartAsync().DefaultTimeout(); @@ -415,7 +414,7 @@ namespace Interop.FunctionalTests.Http3 }, protocol: protocol); using (var host = builder.Build()) - using (var client = CreateClient()) + using (var client = Http3Helpers.CreateClient()) { await host.StartAsync().DefaultTimeout(); @@ -481,7 +480,7 @@ namespace Interop.FunctionalTests.Http3 }); using (var host = builder.Build()) - using (var client = CreateClient()) + using (var client = Http3Helpers.CreateClient()) { await host.StartAsync(); @@ -533,7 +532,7 @@ namespace Interop.FunctionalTests.Http3 }); using (var host = builder.Build()) - using (var client = CreateClient()) + using (var client = Http3Helpers.CreateClient()) { await host.StartAsync(); @@ -599,7 +598,7 @@ namespace Interop.FunctionalTests.Http3 }); using (var host = builder.Build()) - using (var client = CreateClient()) + using (var client = Http3Helpers.CreateClient()) { await host.StartAsync(); @@ -652,7 +651,7 @@ namespace Interop.FunctionalTests.Http3 }); using (var host = builder.Build()) - using (var client = CreateClient()) + using (var client = Http3Helpers.CreateClient()) { await host.StartAsync(); @@ -713,7 +712,7 @@ namespace Interop.FunctionalTests.Http3 }); using (var host = builder.Build()) - using (var client = CreateClient()) + using (var client = Http3Helpers.CreateClient()) { await host.StartAsync(); @@ -778,7 +777,7 @@ namespace Interop.FunctionalTests.Http3 { await host.StartAsync(); - var client = CreateClient(); + var client = Http3Helpers.CreateClient(); try { var port = host.GetPort(); @@ -838,7 +837,7 @@ namespace Interop.FunctionalTests.Http3 }); using (var host = builder.Build()) - using (var client = CreateClient()) + using (var client = Http3Helpers.CreateClient()) { await host.StartAsync(); @@ -943,7 +942,7 @@ namespace Interop.FunctionalTests.Http3 }); using (var host = builder.Build()) - using (var client = CreateClient()) + using (var client = Http3Helpers.CreateClient()) { await host.StartAsync(); @@ -1020,7 +1019,7 @@ namespace Interop.FunctionalTests.Http3 }); using (var host = builder.Build()) - using (var client = CreateClient()) + using (var client = Http3Helpers.CreateClient()) { await host.StartAsync(); @@ -1077,7 +1076,7 @@ namespace Interop.FunctionalTests.Http3 }, protocol: protocol); using (var host = builder.Build()) - using (var client = CreateClient()) + using (var client = Http3Helpers.CreateClient()) { await host.StartAsync().DefaultTimeout(); @@ -1172,7 +1171,7 @@ namespace Interop.FunctionalTests.Http3 }, protocol: protocol); using (var host = builder.Build()) - using (var client = CreateClient()) + using (var client = Http3Helpers.CreateClient()) { await host.StartAsync().DefaultTimeout(); @@ -1215,60 +1214,9 @@ namespace Interop.FunctionalTests.Http3 } } - private static HttpMessageInvoker CreateClient(TimeSpan? idleTimeout = null) - { - var handler = new SocketsHttpHandler(); - handler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions - { - RemoteCertificateValidationCallback = (_, __, ___, ____) => true - }; - if (idleTimeout != null) - { - handler.PooledConnectionIdleTimeout = idleTimeout.Value; - } - - return new HttpMessageInvoker(handler); - } - private IHostBuilder CreateHostBuilder(RequestDelegate requestDelegate, HttpProtocols? protocol = null, Action<KestrelServerOptions> configureKestrel = null) { - return new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .UseKestrel(o => - { - if (configureKestrel == null) - { - o.Listen(IPAddress.Parse("127.0.0.1"), 0, listenOptions => - { - listenOptions.Protocols = protocol ?? HttpProtocols.Http3; - listenOptions.UseHttps(); - }); - } - else - { - configureKestrel(o); - } - }) - .Configure(app => - { - app.Run(requestDelegate); - }); - }) - .ConfigureServices(AddTestLogging) - .ConfigureHostOptions(o => - { - if (Debugger.IsAttached) - { - // Avoid timeout while debugging. - o.ShutdownTimeout = TimeSpan.FromHours(1); - } - else - { - o.ShutdownTimeout = TimeSpan.FromSeconds(1); - } - }); + return Http3Helpers.CreateHostBuilder(AddTestLogging, requestDelegate, protocol, configureKestrel); } } } diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs new file mode 100644 index 00000000000..234e32cf81b --- /dev/null +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs @@ -0,0 +1,69 @@ +// 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; +using System.Net.Http; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Interop.FunctionalTests.Http3 +{ + [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/35070")] + public class Http3TlsTests : LoggedTest + { + [ConditionalFact] + [MsQuicSupported] + public async Task ServerCertificateSelector_Invoked() + { + var builder = CreateHostBuilder(async context => + { + await context.Response.WriteAsync("Hello World"); + }, configureKestrel: kestrelOptions => + { + kestrelOptions.ListenAnyIP(0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http3; + listenOptions.UseHttps(httpsOptions => + { + httpsOptions.ServerCertificateSelector = (context, host) => + { + Assert.Null(context); // The context isn't available durring the quic handshake. + Assert.Equal("localhost", host); + return TestResources.GetTestCertificate(); + }; + }); + }); + }); + + using var host = builder.Build(); + using var client = Http3Helpers.CreateClient(); + + await host.StartAsync().DefaultTimeout(); + + // Using localhost instead of 127.0.0.1 because IPs don't set SNI and the Host header isn't currently used as an override. + var request = new HttpRequestMessage(HttpMethod.Get, $"https://localhost:{host.GetPort()}/"); + request.Version = HttpVersion.Version30; + request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + // 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); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpVersion.Version30, response.Version); + Assert.Equal("Hello World", result); + + await host.StopAsync().DefaultTimeout(); + } + + private IHostBuilder CreateHostBuilder(RequestDelegate requestDelegate, HttpProtocols? protocol = null, Action<KestrelServerOptions> configureKestrel = null) + { + return Http3Helpers.CreateHostBuilder(AddTestLogging, requestDelegate, protocol, configureKestrel); + } + } +} -- GitLab