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/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/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 2ea73a318584..681cac04dfff 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); - return listenOptions.UseHttps(options); + if (!options.HasServerCertificateOrSelector) + { + throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound); + } + + return options; + })); } /// @@ -188,17 +191,30 @@ 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; + + var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); + 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 => { - // Set the list of protocols from listen options - httpsOptions.HttpProtocols = listenOptions.Protocols; - var middleware = new HttpsConnectionMiddleware(next, httpsOptions, loggerFactory, metrics); + // Evaluate the HttpsConnectionAdapterOptions, now that the configuration, if any, has been loaded + var httpsOptions = lazyHttpsOptions.Value; + var middleware = new HttpsConnectionMiddleware(next, httpsOptions, listenOptions.Protocols, loggerFactory, metrics); return middleware.OnConnectionAsync; }); @@ -259,6 +275,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/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/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/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) 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);