From 90ddbbc994b7a0f0fb1b7ad9ad72188d5f4c954a Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Mon, 1 May 2023 16:57:14 -0700 Subject: [PATCH 1/3] Move HttpProtocols from HttpsConnectionAdapterOptions to HttpsConnectionMiddleware --- .../Core/src/HttpsConnectionAdapterOptions.cs | 6 ---- .../Core/src/ListenOptionsHttpsExtensions.cs | 4 +-- .../Middleware/HttpsConnectionMiddleware.cs | 14 ++++---- .../HttpsConnectionMiddlewareTests.cs | 36 +++++++++---------- 4 files changed, 27 insertions(+), 33 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs b/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs index 48b6629d0762..f13540fa579c 100644 --- a/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs +++ b/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs @@ -77,12 +77,6 @@ public HttpsConnectionAdapterOptions() /// public SslProtocols SslProtocols { get; set; } - /// - /// The protocols enabled on this endpoint. - /// - /// Defaults to HTTP/1.x only. - internal HttpProtocols HttpProtocols { get; set; } - /// /// Specifies whether the certificate revocation list is checked during authentication. /// diff --git a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs index 2ea73a318584..9495ed30b72a 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs @@ -196,9 +196,7 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsConn listenOptions.Use(next => { - // Set the list of protocols from listen options - httpsOptions.HttpProtocols = listenOptions.Protocols; - var middleware = new HttpsConnectionMiddleware(next, httpsOptions, loggerFactory, metrics); + var middleware = new HttpsConnectionMiddleware(next, httpsOptions, listenOptions.Protocols, loggerFactory, metrics); return middleware.OnConnectionAsync; }); diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs index 3488024f2294..a25f026ecc3a 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs @@ -33,6 +33,9 @@ internal sealed class HttpsConnectionMiddleware private readonly ILogger _logger; private readonly Func _sslStreamFactory; + // Internal for testing + internal readonly HttpProtocols _httpProtocols; + // The following fields are only set by HttpsConnectionAdapterOptions ctor. private readonly HttpsConnectionAdapterOptions? _options; private readonly KestrelMetrics _metrics; @@ -43,17 +46,16 @@ internal sealed class HttpsConnectionMiddleware // The following fields are only set by TlsHandshakeCallbackOptions ctor. private readonly Func>? _tlsCallbackOptions; private readonly object? _tlsCallbackOptionsState; - private readonly HttpProtocols _httpProtocols; // Pool for cancellation tokens that cancel the handshake private readonly CancellationTokenSourcePool _ctsPool = new(); - public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapterOptions options, KestrelMetrics metrics) - : this(next, options, loggerFactory: NullLoggerFactory.Instance, metrics: metrics) + 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, ILoggerFactory loggerFactory, KestrelMetrics metrics) + public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapterOptions options, HttpProtocols httpProtocols, ILoggerFactory loggerFactory, KestrelMetrics metrics) { ArgumentNullException.ThrowIfNull(options); @@ -74,7 +76,7 @@ public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapter //_sslStreamFactory = s => new SslStream(s); _options = options; - _options.HttpProtocols = ValidateAndNormalizeHttpProtocols(_options.HttpProtocols, _logger); + _httpProtocols = ValidateAndNormalizeHttpProtocols(httpProtocols, _logger); // capture the certificate now so it can't be switched after validation _serverCertificate = options.ServerCertificate; @@ -331,7 +333,7 @@ private Task DoOptionsBasedHandshakeAsync(ConnectionContext context, SslStream s CertificateRevocationCheckMode = _options.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck, }; - ConfigureAlpn(sslOptions, _options.HttpProtocols); + ConfigureAlpn(sslOptions, _httpProtocols); _options.OnAuthenticate?.Invoke(context, sslOptions); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs index f51b31631c4a..3362f78150ea 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs @@ -264,7 +264,7 @@ void ConfigureListenOptions(ListenOptions listenOptions) [Fact] public void ThrowsWhenNoServerCertificateIsProvided() { - Assert.Throws(() => CreateMiddleware(new HttpsConnectionAdapterOptions())); + Assert.Throws(() => CreateMiddleware(new HttpsConnectionAdapterOptions(), ListenOptions.DefaultHttpProtocols)); } [Fact] @@ -1318,7 +1318,8 @@ public void ValidatesEnhancedKeyUsageOnCertificate(string testCertName) CreateMiddleware(new HttpsConnectionAdapterOptions { ServerCertificate = cert, - }); + }, + ListenOptions.DefaultHttpProtocols); } [Theory] @@ -1337,7 +1338,8 @@ public void ThrowsForCertificatesMissingServerEku(string testCertName) CreateMiddleware(new HttpsConnectionAdapterOptions { ServerCertificate = cert, - })); + }, + ListenOptions.DefaultHttpProtocols)); Assert.Equal(CoreStrings.FormatInvalidServerCertificateEku(cert.Thumbprint), ex.Message); } @@ -1357,6 +1359,7 @@ public void LogsForCertificateMissingSubjectAlternativeName(string testCertName) { ServerCertificate = cert, }, + ListenOptions.DefaultHttpProtocols, testLogger); Assert.Single(testLogger.Messages.Where(log => log.EventId == 9)); @@ -1404,11 +1407,10 @@ public void Http1AndHttp2DowngradeToHttp1ForHttpsOnIncompatibleWindowsVersions() var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2, - HttpProtocols = HttpProtocols.Http1AndHttp2 }; - CreateMiddleware(httpConnectionAdapterOptions); + var middleware = CreateMiddleware(httpConnectionAdapterOptions, HttpProtocols.Http1AndHttp2); - Assert.Equal(HttpProtocols.Http1, httpConnectionAdapterOptions.HttpProtocols); + Assert.Equal(HttpProtocols.Http1, middleware._httpProtocols); } [ConditionalFact] @@ -1419,11 +1421,10 @@ public void Http1AndHttp2DoesNotDowngradeOnCompatibleWindowsVersions() var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2, - HttpProtocols = HttpProtocols.Http1AndHttp2 }; - CreateMiddleware(httpConnectionAdapterOptions); + var middleware = CreateMiddleware(httpConnectionAdapterOptions, HttpProtocols.Http1AndHttp2); - Assert.Equal(HttpProtocols.Http1AndHttp2, httpConnectionAdapterOptions.HttpProtocols); + Assert.Equal(HttpProtocols.Http1AndHttp2, middleware._httpProtocols); } [ConditionalFact] @@ -1434,10 +1435,9 @@ public void Http2ThrowsOnIncompatibleWindowsVersions() var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2, - HttpProtocols = HttpProtocols.Http2 }; - Assert.Throws(() => CreateMiddleware(httpConnectionAdapterOptions)); + Assert.Throws(() => CreateMiddleware(httpConnectionAdapterOptions, HttpProtocols.Http2)); } [ConditionalFact] @@ -1448,11 +1448,10 @@ public void Http2DoesNotThrowOnCompatibleWindowsVersions() var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2, - HttpProtocols = HttpProtocols.Http2 }; // Does not throw - CreateMiddleware(httpConnectionAdapterOptions); + CreateMiddleware(httpConnectionAdapterOptions, HttpProtocols.Http2); } private static HttpsConnectionMiddleware CreateMiddleware(X509Certificate2 serverCertificate) @@ -1460,18 +1459,19 @@ private static HttpsConnectionMiddleware CreateMiddleware(X509Certificate2 serve return CreateMiddleware(new HttpsConnectionAdapterOptions { ServerCertificate = serverCertificate, - }); + }, + ListenOptions.DefaultHttpProtocols); } - private static HttpsConnectionMiddleware CreateMiddleware(HttpsConnectionAdapterOptions options, TestApplicationErrorLogger testLogger = null) + private static HttpsConnectionMiddleware CreateMiddleware(HttpsConnectionAdapterOptions options, HttpProtocols httpProtocols, TestApplicationErrorLogger testLogger = null) { var loggerFactory = testLogger is null ? (ILoggerFactory)NullLoggerFactory.Instance : new LoggerFactory(new[] { new KestrelTestLoggerProvider(testLogger) }); - return new HttpsConnectionMiddleware(context => Task.CompletedTask, options, loggerFactory, new KestrelMetrics(new TestMeterFactory())); + return new HttpsConnectionMiddleware(context => Task.CompletedTask, options, httpProtocols, loggerFactory, new KestrelMetrics(new TestMeterFactory())); } - private static HttpsConnectionMiddleware CreateMiddleware(HttpsConnectionAdapterOptions options) + private static HttpsConnectionMiddleware CreateMiddleware(HttpsConnectionAdapterOptions options, HttpProtocols httpProtocols) { - return new HttpsConnectionMiddleware(context => Task.CompletedTask, options, new KestrelMetrics(new TestMeterFactory())); + return new HttpsConnectionMiddleware(context => Task.CompletedTask, options, httpProtocols, new KestrelMetrics(new TestMeterFactory())); } private static async Task App(HttpContext httpContext) From f8d3cd0925d60a504612d7c76f7097d7ad5c50ac Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Tue, 2 May 2023 11:24:53 -0700 Subject: [PATCH 2/3] Defer search for dev cert until build bind time ...so that it can occur after IConfiguration is read. (In particular, so that it occurs during AddressBinder.BindAsync which happens strictly after KestrelConfigurationLoader.Load.) This is important in Docker scenarios since the Docker tools use IConfiguration to tell us where the dev cert directory is mounted.# with '#' will be ignored, and an empty message aborts the commit. Was #46296 Fixes #45801 --- .../Core/src/HttpsConfigurationService.cs | 2 +- src/Servers/Kestrel/Core/src/ListenOptions.cs | 2 +- .../Core/src/ListenOptionsHttpsExtensions.cs | 42 +++++-- .../test/KestrelConfigurationLoaderTests.cs | 104 +++++++++++++++++- .../InMemory.FunctionalTests/HttpsTests.cs | 16 +++ .../Http3/Http3TlsTests.cs | 92 ++++++++++++++++ 6 files changed, 243 insertions(+), 15 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs b/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs index 013833a6463e..21285d07d3a8 100644 --- a/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs +++ b/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs @@ -147,7 +147,7 @@ internal static void PopulateMultiplexedTransportFeaturesWorker(FeatureCollectio // The QUIC transport will check if TlsConnectionCallbackOptions is missing. if (listenOptions.HttpsOptions != null) { - var sslServerAuthenticationOptions = HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions, logger); + var sslServerAuthenticationOptions = HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions.Value, logger); features.Set(new TlsConnectionCallbackOptions { ApplicationProtocols = sslServerAuthenticationOptions.ApplicationProtocols ?? new List { SslApplicationProtocol.Http3 }, diff --git a/src/Servers/Kestrel/Core/src/ListenOptions.cs b/src/Servers/Kestrel/Core/src/ListenOptions.cs index b8aaf3c75d72..345e6bd17826 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptions.cs @@ -140,7 +140,7 @@ internal string Scheme } internal bool IsTls { get; set; } - internal HttpsConnectionAdapterOptions? HttpsOptions { get; set; } + internal Lazy? HttpsOptions { get; set; } internal TlsHandshakeCallbackOptions? HttpsCallbackOptions { get; set; } /// diff --git a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs index 9495ed30b72a..dff985416d68 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs @@ -166,17 +166,20 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, Action(() => { - throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound); - } + var options = new HttpsConnectionAdapterOptions(); + listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options); + configureOptions(options); + listenOptions.KestrelServerOptions.ApplyDefaultCertificate(options); + + if (!options.HasServerCertificateOrSelector) + { + throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound); + } - return listenOptions.UseHttps(options); + return options; + })); } /// @@ -188,14 +191,28 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, ActionThe . public static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions) { - var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); - var metrics = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); + return listenOptions.UseHttps(new Lazy(httpsOptions)); + } + /// + /// Configure Kestrel to use HTTPS. This does not use default certificates or other defaults specified via config or + /// . + /// + /// The to configure. + /// Options to configure HTTPS. + /// The . + private static ListenOptions UseHttps(this ListenOptions listenOptions, Lazy lazyHttpsOptions) + { listenOptions.IsTls = true; - listenOptions.HttpsOptions = httpsOptions; + listenOptions.HttpsOptions = lazyHttpsOptions; + // NB: This lambda will only be invoked if either HTTP/1.* or HTTP/2 is being used listenOptions.Use(next => { + // Evaluate the HttpsConnectionAdapterOptions, now that the configuration, if any, has been loaded + var httpsOptions = lazyHttpsOptions.Value; + var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); + var metrics = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); var middleware = new HttpsConnectionMiddleware(next, httpsOptions, listenOptions.Protocols, loggerFactory, metrics); return middleware.OnConnectionAsync; }); @@ -257,6 +274,7 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, TlsHandsh listenOptions.IsTls = true; listenOptions.HttpsCallbackOptions = callbackOptions; + // NB: This lambda will only be invoked if either HTTP/1.* or HTTP/2 is being used listenOptions.Use(next => { // Set the list of protocols from listen options. diff --git a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs index 5b033b4952ef..11b9ac23efa2 100644 --- a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs @@ -380,7 +380,105 @@ public void ConfigureEndpoint_RecoverFromBadPassword() void CheckListenOptions(X509Certificate2 expectedCert) { var listenOptions = Assert.Single(serverOptions.ConfigurationBackedListenOptions); - Assert.Equal(expectedCert.SerialNumber, listenOptions.HttpsOptions.ServerCertificate.SerialNumber); + Assert.Equal(expectedCert.SerialNumber, listenOptions.HttpsOptions!.Value.ServerCertificate.SerialNumber); + } + } + + [Fact] + public void LoadDevelopmentCertificate_ConfigureFirst() + { + try + { + var serverOptions = CreateServerOptions(); + var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable); + var bytes = certificate.Export(X509ContentType.Pkcs12, "1234"); + var path = GetCertificatePath(); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + File.WriteAllBytes(path, bytes); + + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Certificates:Development:Password", "1234"), + }).Build(); + + serverOptions.Configure(config); + + Assert.Null(serverOptions.ConfigurationLoader.DefaultCertificate); + + serverOptions.ConfigurationLoader.Load(); + + Assert.NotNull(serverOptions.ConfigurationLoader.DefaultCertificate); + Assert.Equal(serverOptions.ConfigurationLoader.DefaultCertificate.SerialNumber, certificate.SerialNumber); + + var ran1 = false; + serverOptions.ListenAnyIP(4545, listenOptions => + { + ran1 = true; + listenOptions.UseHttps(); + }); + Assert.True(ran1); + + var listenOptions = serverOptions.CodeBackedListenOptions.Single(); + Assert.False(listenOptions.HttpsOptions.IsValueCreated); + listenOptions.Build(); + Assert.True(listenOptions.HttpsOptions.IsValueCreated); + Assert.Equal(listenOptions.HttpsOptions.Value.ServerCertificate?.SerialNumber, certificate.SerialNumber); + } + finally + { + if (File.Exists(GetCertificatePath())) + { + File.Delete(GetCertificatePath()); + } + } + } + + [Fact] + public void LoadDevelopmentCertificate_UseHttpsFirst() + { + try + { + var serverOptions = CreateServerOptions(); + var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable); + var bytes = certificate.Export(X509ContentType.Pkcs12, "1234"); + var path = GetCertificatePath(); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + File.WriteAllBytes(path, bytes); + + var ran1 = false; + serverOptions.ListenAnyIP(4545, listenOptions => + { + ran1 = true; + listenOptions.UseHttps(); + }); + Assert.True(ran1); + + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Certificates:Development:Password", "1234"), + }).Build(); + + serverOptions.Configure(config); + + Assert.Null(serverOptions.ConfigurationLoader.DefaultCertificate); + + serverOptions.ConfigurationLoader.Load(); + + Assert.NotNull(serverOptions.ConfigurationLoader.DefaultCertificate); + Assert.Equal(serverOptions.ConfigurationLoader.DefaultCertificate.SerialNumber, certificate.SerialNumber); + + var listenOptions = serverOptions.CodeBackedListenOptions.Single(); + Assert.False(listenOptions.HttpsOptions.IsValueCreated); + listenOptions.Build(); + Assert.True(listenOptions.HttpsOptions.IsValueCreated); + Assert.Equal(listenOptions.HttpsOptions.Value.ServerCertificate?.SerialNumber, certificate.SerialNumber); + } + finally + { + if (File.Exists(GetCertificatePath())) + { + File.Delete(GetCertificatePath()); + } } } @@ -862,6 +960,8 @@ public void EndpointConfigureSection_CanSetSslProtocol() }); }); + _ = serverOptions.CodeBackedListenOptions.Single().HttpsOptions.Value; // Force evaluation + Assert.True(ranDefault); Assert.True(ran1); Assert.True(ran2); @@ -997,6 +1097,8 @@ public void EndpointConfigureSection_CanSetClientCertificateMode() }); }); + _ = serverOptions.CodeBackedListenOptions.Single().HttpsOptions.Value; // Force evaluation + Assert.True(ranDefault); Assert.True(ran1); Assert.True(ran2); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs index 819def76e5a2..459753c04330 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs @@ -64,14 +64,18 @@ public void UseHttpsDefaultsToDefaultCert() Assert.False(serverOptions.IsDevelopmentCertificateLoaded); + var ranUseHttpsAction = false; serverOptions.ListenLocalhost(5001, options => { options.UseHttps(opt => { // The default cert is applied after UseHttps. Assert.Null(opt.ServerCertificate); + ranUseHttpsAction = true; }); }); + _ = serverOptions.CodeBackedListenOptions[1].HttpsOptions.Value; // Force evaluation + Assert.True(ranUseHttpsAction); Assert.False(serverOptions.IsDevelopmentCertificateLoaded); } @@ -117,14 +121,20 @@ public void ConfigureHttpsDefaultsNeverLoadsDefaultCert() options.ServerCertificate = _x509Certificate2; options.ClientCertificateMode = ClientCertificateMode.RequireCertificate; }); + var ranUseHttpsAction = false; serverOptions.ListenLocalhost(5000, options => { options.UseHttps(opt => { Assert.Equal(_x509Certificate2, opt.ServerCertificate); Assert.Equal(ClientCertificateMode.RequireCertificate, opt.ClientCertificateMode); + ranUseHttpsAction = true; }); }); + + _ = serverOptions.CodeBackedListenOptions.Single().HttpsOptions.Value; // Force evaluation + Assert.True(ranUseHttpsAction); + // Never lazy loaded Assert.False(serverOptions.IsDevelopmentCertificateLoaded); Assert.Null(serverOptions.DevelopmentCertificate); @@ -144,6 +154,7 @@ public void ConfigureCertSelectorNeverLoadsDefaultCert() }; options.ClientCertificateMode = ClientCertificateMode.RequireCertificate; }); + var ranUseHttpsAction = false; serverOptions.ListenLocalhost(5000, options => { options.UseHttps(opt => @@ -151,8 +162,13 @@ public void ConfigureCertSelectorNeverLoadsDefaultCert() Assert.Null(opt.ServerCertificate); Assert.NotNull(opt.ServerCertificateSelector); Assert.Equal(ClientCertificateMode.RequireCertificate, opt.ClientCertificateMode); + ranUseHttpsAction = true; }); }); + + _ = serverOptions.CodeBackedListenOptions.Single().HttpsOptions.Value; // Force evaluation + Assert.True(ranUseHttpsAction); + // Never lazy loaded Assert.False(serverOptions.IsDevelopmentCertificateLoaded); Assert.Null(serverOptions.DevelopmentCertificate); diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs index 8efd8e2789bd..a03253369e84 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs @@ -5,6 +5,7 @@ using System.Net.Http; using System.Net.Quic; using System.Net.Security; +using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -25,6 +26,7 @@ public class Http3TlsTests : LoggedTest [MsQuicSupported] public async Task ServerCertificateSelector_Invoked() { + var serverCertificateSelectorActionCalled = false; var builder = CreateHostBuilder(async context => { await context.Response.WriteAsync("Hello World"); @@ -37,6 +39,7 @@ public async Task ServerCertificateSelector_Invoked() { httpsOptions.ServerCertificateSelector = (context, host) => { + serverCertificateSelectorActionCalled = true; Assert.Null(context); // The context isn't available durring the quic handshake. Assert.Equal("testhost", host); return TestResources.GetTestCertificate(); @@ -61,6 +64,8 @@ public async Task ServerCertificateSelector_Invoked() Assert.Equal(HttpVersion.Version30, response.Version); Assert.Equal("Hello World", result); + Assert.True(serverCertificateSelectorActionCalled); + await host.StopAsync().DefaultTimeout(); } @@ -422,6 +427,93 @@ public void UseKestrelCore_ConfigurationBased(bool useQuic) Assert.Throws(host.Run); } + [ConditionalFact] + [MsQuicSupported] + public async Task LoadDevelopmentCertificateViaConfiguration() + { + var expectedCertificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable); + var bytes = expectedCertificate.Export(X509ContentType.Pkcs12, "1234"); + var path = GetCertificatePath(); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + File.WriteAllBytes(path, bytes); + + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Certificates:Development:Password", "1234"), + }).Build(); + + var ranConfigureKestrelAction = false; + var ranUseHttpsAction = false; + var hostBuilder = CreateHostBuilder(async context => + { + await context.Response.WriteAsync("Hello World"); + }, configureKestrel: kestrelOptions => + { + ranConfigureKestrelAction = true; + kestrelOptions.Configure(config); + + kestrelOptions.ListenAnyIP(0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http3; + listenOptions.UseHttps(_ => + { + ranUseHttpsAction = true; + }); + }); + }); + + Assert.False(ranConfigureKestrelAction); + Assert.False(ranUseHttpsAction); + + using var host = hostBuilder.Build(); + await host.StartAsync().DefaultTimeout(); + + Assert.True(ranConfigureKestrelAction); + Assert.True(ranUseHttpsAction); + + var request = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/"); + request.Version = HttpVersion.Version30; + request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + request.Headers.Host = "testhost"; + + var ranCertificateValidation = false; + var httpHandler = new SocketsHttpHandler(); + httpHandler.SslOptions = new SslClientAuthenticationOptions + { + RemoteCertificateValidationCallback = (object _sender, X509Certificate actualCertificate, X509Chain _chain, SslPolicyErrors _sslPolicyErrors) => + { + ranCertificateValidation = true; + Assert.Equal(expectedCertificate.GetSerialNumberString(), actualCertificate.GetSerialNumberString()); + return true; + }, + TargetHost = "targethost", + }; + using var client = new HttpMessageInvoker(httpHandler); + + 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("Hello World", result); + + Assert.True(ranCertificateValidation); + + await host.StopAsync().DefaultTimeout(); + } + + /// + /// This is something of a hack - we should actually be calling + /// . + /// + private static string GetCertificatePath() + { + var appData = Environment.GetEnvironmentVariable("APPDATA"); + var home = Environment.GetEnvironmentVariable("HOME"); + var basePath = appData != null ? Path.Combine(appData, "ASP.NET", "https") : null; + basePath = basePath ?? (home != null ? Path.Combine(home, ".aspnet", "https") : null); + return Path.Combine(basePath, $"{typeof(Http3TlsTests).Assembly.GetName().Name}.pfx"); + } + private IHostBuilder CreateHostBuilder(RequestDelegate requestDelegate, HttpProtocols? protocol = null, Action configureKestrel = null) { return HttpHelpers.CreateHostBuilder(AddTestLogging, requestDelegate, protocol, configureKestrel); From 904aec8907ceef3d691047859ec520a85db85ac0 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Thu, 4 May 2023 10:29:16 -0700 Subject: [PATCH 3/3] Pull DI lookups out of lambda --- src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs index dff985416d68..681cac04dfff 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs @@ -206,13 +206,14 @@ private static ListenOptions UseHttps(this ListenOptions listenOptions, Lazy(); + var metrics = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); + // NB: This lambda will only be invoked if either HTTP/1.* or HTTP/2 is being used listenOptions.Use(next => { // Evaluate the HttpsConnectionAdapterOptions, now that the configuration, if any, has been loaded var httpsOptions = lazyHttpsOptions.Value; - var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); - var metrics = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); var middleware = new HttpsConnectionMiddleware(next, httpsOptions, listenOptions.Protocols, loggerFactory, metrics); return middleware.OnConnectionAsync; });