Browse Source

Make TLS & QUIC Pay-for-Play (Redux) (#47454)

* Make TLS & QUIC pay-for-play

`CreateSlimBuilder` now calls `UseKestrelSlim` (name TBD) to create a Kestrel server that doesn't automatically support TLS or QUIC.  If you invoke `ListenOptions.UseHttps`, TLS will "just work" but, if you want to enable https using `ASPNETCORE_URLS`, you'll need to invoke the new `WebHostBuilder.UseHttpsConfiguration` extension method.  Quic is enabled using the existing `WebHostBuilder.UseQuic` extension method.

Squash:

Break direct dependency of AddressBinder on ListenOptionsHttpsExtensions.UseHttps

Factor out the part of TransportManager that depends on https

Introduce KestrelServerOptions.HasServerCertificateOrSelector for convenience

Factor TlsConfigurationLoader out of KestrelConfigurationLoader

Introduce but don't consume IHttpsConfigurationHelper

Consume IHttpsConfigurationHelper - tests failing

Fix most tests

Fix KestrelServerTests

Fix remaining tests

Respect IHttpsConfigurationHelper in ApplyDefaultCertificate

Introduce UseKestrelSlim

Delete unused TryUseHttps

Enable HttpsConfiguration when UseHttps is called

Introduce UseHttpsConfiguration

Drop incomplete test implementation of IHttpsConfigurationHelper

Tidy up test diffs

Fix AOT trimming by moving enable call out of ctor

Fix some tests

Simplify HttpsConfigurationHelper ctor for more convenient testing

Improve error message

Don't declare Enabler transient

Fix tests other than KestrelConfigurationLoaderTests

Correct HttpsConfigurationHelper

Add IEnabler interface to break direct dependency

Restore UseKestrel call in WebHost.ConfigureWebDefaults

Stop registering an https address in ApiTemplateTest

boolean -> bool

HttpsConfigurationHelper -> HttpsConfigurationService

HttpsConfigurationService.Enable -> Initialize

ITlsConfigurationLoader.ApplyHttpsDefaults -> ApplyHttpsConfiguration

ITlsConfigurationLoader.UseHttps -> UseHttpsWithSni

IHttpsConfigurationService.UseHttps -> UseHttpsWithDefaults

Inline ITlsConfigurationLoader in IHttpsConfigurationService

Document IHttpsConfigurationService

Document new public APIs in WebHostBuilderKestrelExtensions

Clean up TODOs

Improve error text recommending UseQuic

Co-authored-by: James Newton-King <[email protected]>

Add assert message

Clarify comment on assert

Fix typo in doc comment

Co-authored-by: Aditya Mandaleeka <[email protected]>

Fix typo in doc comment

Co-authored-by: Aditya Mandaleeka <[email protected]>

Fix typo in doc comment

Co-authored-by: Aditya Mandaleeka <[email protected]>

Don't use regions

Correct casing

Replace record with readonly struct

Test AddressBinder exception

Test an endpoint address from config

Test certificate loading

Bonus: use dynamic ports to improve reliability

Test Quic with UseKestrelSlim

Test the interaction of UseHttps and UseHttpsConfiguration

Test different UseHttps overloads

Add more detail to doc comment

Set TestOverrideDefaultCertificate in the tests that expect it

* Improve assert message

Co-authored-by: James Newton-King <[email protected]>

* Adopt MemberNotNullAttribute

* Assert that members are non-null to suppress CS8774

* UseHttpsConfiguration -> UseKestrelHttpsConfiguration

* UseKestrelSlim -> UseKestrelCore

* Drop convenience overloads of UseKestrelCore

* Use more explicit error strings in HttpsConfigurationService.EnsureInitialized

---------

Co-authored-by: James Newton-King <[email protected]>
Andrew Casey 2 years ago
parent
commit
f1bbdd4ce9
29 changed files with 1160 additions and 331 deletions
  1. 19 10
      src/DefaultBuilder/src/WebHost.cs
  2. 5 4
      src/ProjectTemplates/Shared/Project.cs
  3. 3 2
      src/ProjectTemplates/test/Templates.Tests/ApiTemplateTest.cs
  4. 13 1
      src/Servers/Kestrel/Core/src/CoreStrings.resx
  5. 260 0
      src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs
  6. 5 0
      src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs
  7. 100 0
      src/Servers/Kestrel/Core/src/IHttpsConfigurationService.cs
  8. 13 11
      src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs
  9. 5 77
      src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs
  10. 8 4
      src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs
  11. 1 1
      src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs
  12. 20 167
      src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs
  13. 45 0
      src/Servers/Kestrel/Core/src/KestrelServer.cs
  14. 20 6
      src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
  15. 4 17
      src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs
  16. 1 1
      src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
  17. 205 0
      src/Servers/Kestrel/Core/src/TlsConfigurationLoader.cs
  18. 8 14
      src/Servers/Kestrel/Core/test/AddressBinderTests.cs
  19. 1 0
      src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs
  20. 19 1
      src/Servers/Kestrel/Core/test/KestrelServerTests.cs
  21. 2 0
      src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt
  22. 50 8
      src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs
  23. 237 0
      src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs
  24. 2 0
      src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs
  25. 3 1
      src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs
  26. 7 6
      src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs
  27. 8 0
      src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs
  28. 3 0
      src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs
  29. 93 0
      src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs

+ 19 - 10
src/DefaultBuilder/src/WebHost.cs

@@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Hosting.StaticWebAssets;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
@@ -223,23 +224,31 @@ public static class WebHost
             }
         });
 
-        ConfigureWebDefaultsCore(builder, services =>
-        {
-            services.AddRouting();
-        });
+        ConfigureWebDefaultsWorker(
+            builder.UseKestrel(ConfigureKestrel),
+            services =>
+            {
+                services.AddRouting();
+            });
 
         builder
             .UseIIS()
             .UseIISIntegration();
     }
 
-    internal static void ConfigureWebDefaultsCore(IWebHostBuilder builder, Action<IServiceCollection>? configureRouting = null)
+    internal static void ConfigureWebDefaultsCore(IWebHostBuilder builder)
     {
-        builder.UseKestrel((builderContext, options) =>
-        {
-            options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true);
-        })
-        .ConfigureServices((hostingContext, services) =>
+        ConfigureWebDefaultsWorker(builder.UseKestrelCore().ConfigureKestrel(ConfigureKestrel), configureRouting: null);
+    }
+
+    private static void ConfigureKestrel(WebHostBuilderContext builderContext, KestrelServerOptions options)
+    {
+        options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true);
+    }
+
+    private static void ConfigureWebDefaultsWorker(IWebHostBuilder builder, Action<IServiceCollection>? configureRouting)
+    {
+        builder.ConfigureServices((hostingContext, services) =>
         {
             // Fallback
             services.PostConfigure<HostFilteringOptions>(options =>

+ 5 - 4
src/ProjectTemplates/Shared/Project.cs

@@ -24,6 +24,7 @@ namespace Templates.Test.Helpers;
 [DebuggerDisplay("{ToString(),nq}")]
 public class Project : IDisposable
 {
+    private const string _urlsNoHttps = "http://127.0.0.1:0";
     private const string _urls = "http://127.0.0.1:0;https://127.0.0.1:0";
 
     public static string ArtifactsLogDir
@@ -181,11 +182,11 @@ public class Project : IDisposable
         Assert.True(0 == result.ExitCode, ErrorMessages.GetFailedProcessMessage("build", this, result));
     }
 
-    internal AspNetProcess StartBuiltProjectAsync(bool hasListeningUri = true, ILogger logger = null)
+    internal AspNetProcess StartBuiltProjectAsync(bool hasListeningUri = true, ILogger logger = null, bool noHttps = false)
     {
         var environment = new Dictionary<string, string>
         {
-            ["ASPNETCORE_URLS"] = _urls,
+            ["ASPNETCORE_URLS"] = noHttps ? _urlsNoHttps : _urls,
             ["ASPNETCORE_ENVIRONMENT"] = "Development",
             ["ASPNETCORE_Logging__Console__LogLevel__Default"] = "Debug",
             ["ASPNETCORE_Logging__Console__LogLevel__System"] = "Debug",
@@ -197,11 +198,11 @@ public class Project : IDisposable
         return new AspNetProcess(DevCert, Output, TemplateOutputDir, projectDll, environment, published: false, hasListeningUri: hasListeningUri, logger: logger);
     }
 
-    internal AspNetProcess StartPublishedProjectAsync(bool hasListeningUri = true, bool usePublishedAppHost = false)
+    internal AspNetProcess StartPublishedProjectAsync(bool hasListeningUri = true, bool usePublishedAppHost = false, bool noHttps = false)
     {
         var environment = new Dictionary<string, string>
         {
-            ["ASPNETCORE_URLS"] = _urls,
+            ["ASPNETCORE_URLS"] = noHttps ? _urlsNoHttps : _urls,
             ["ASPNETCORE_Logging__Console__LogLevel__Default"] = "Debug",
             ["ASPNETCORE_Logging__Console__LogLevel__System"] = "Debug",
             ["ASPNETCORE_Logging__Console__LogLevel__Microsoft"] = "Debug",

+ 3 - 2
src/ProjectTemplates/test/Templates.Tests/ApiTemplateTest.cs

@@ -82,7 +82,8 @@ public class ApiTemplateTest : LoggedTest
 
         await project.RunDotNetBuildAsync();
 
-        using (var aspNetProcess = project.StartBuiltProjectAsync())
+        // The minimal/slim/core scenario doesn't include TLS support, so tell `project` not to register an https address
+        using (var aspNetProcess = project.StartBuiltProjectAsync(noHttps: true))
         {
             Assert.False(
                aspNetProcess.Process.HasExited,
@@ -91,7 +92,7 @@ public class ApiTemplateTest : LoggedTest
             await AssertEndpoints(aspNetProcess);
         }
 
-        using (var aspNetProcess = project.StartPublishedProjectAsync())
+        using (var aspNetProcess = project.StartPublishedProjectAsync(noHttps: true))
         {
             Assert.False(
                 aspNetProcess.Process.HasExited,

+ 13 - 1
src/Servers/Kestrel/Core/src/CoreStrings.resx

@@ -722,4 +722,16 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
   <data name="FailedToBindToIPv6Any" xml:space="preserve">
     <value>Failed to bind to http://[::]:{port} (IPv6Any).</value>
   </data>
-</root>
+  <data name="NeedHttpsConfigurationToApplyHttpsConfiguration" xml:space="preserve">
+    <value>Call UseKestrelHttpsConfiguration() on IWebHostBuilder to enable loading HTTPS settings from configuration.</value>
+  </data>
+  <data name="NeedHttpsConfigurationToLoadDefaultCertificate" xml:space="preserve">
+    <value>Call UseKestrelHttpsConfiguration() on IWebHostBuilder to enable loading the default server certificate from configuration.</value>
+  </data>
+  <data name="NeedHttpsConfigurationToUseHttp3" xml:space="preserve">
+    <value>Call UseKestrelHttpsConfiguration() on IWebHostBuilder to enable transport layer security for HTTP/3.</value>
+  </data>
+  <data name="NeedHttpsConfigurationToBindHttpsAddresses" xml:space="preserve">
+    <value>Call UseKestrelHttpsConfiguration() on IWebHostBuilder to automatically enable HTTPS when an https:// address is used.</value>
+  </data>
+</root>

+ 260 - 0
src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs

@@ -0,0 +1,260 @@
+// 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.Diagnostics.CodeAnalysis;
+using System.IO.Pipelines;
+using System.Net;
+using System.Net.Security;
+using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
+using Microsoft.AspNetCore.Server.Kestrel.Https;
+using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core;
+
+/// <inheritdoc />
+internal sealed class HttpsConfigurationService : IHttpsConfigurationService
+{
+    private readonly IInitializer? _initializer;
+    private bool _isInitialized;
+
+    private TlsConfigurationLoader? _tlsConfigurationLoader;
+    private Action<FeatureCollection, ListenOptions>? _populateMultiplexedTransportFeatures;
+    private Func<ListenOptions, ListenOptions>? _useHttpsWithDefaults;
+
+    /// <summary>
+    /// Create an uninitialized <see cref="HttpsConfigurationService"/>.
+    /// To initialize it later, call <see cref="Initialize"/>.
+    /// </summary>
+    public HttpsConfigurationService()
+    {
+    }
+
+    /// <summary>
+    /// Create an initialized <see cref="HttpsConfigurationService"/>.
+    /// </summary>
+    /// <remarks>
+    /// In practice, <see cref="Initialize"/> won't be called until it's needed.
+    /// </remarks>
+    public HttpsConfigurationService(IInitializer initializer)
+    {
+        _initializer = initializer;
+    }
+
+    /// <inheritdoc />
+    // If there's an initializer, it *can* be initialized, even though it might not be yet.
+    // Use explicit interface implentation so we don't accidentally call it within this class.
+    bool IHttpsConfigurationService.IsInitialized => _isInitialized || _initializer is not null;
+
+    /// <inheritdoc/>
+    public void Initialize(
+        IHostEnvironment hostEnvironment,
+        ILogger<KestrelServer> serverLogger,
+        ILogger<HttpsConnectionMiddleware> httpsLogger)
+    {
+        if (_isInitialized)
+        {
+            return;
+        }
+
+        _isInitialized = true;
+
+        _tlsConfigurationLoader = new TlsConfigurationLoader(hostEnvironment, serverLogger, httpsLogger);
+        _populateMultiplexedTransportFeatures = PopulateMultiplexedTransportFeaturesWorker;
+        _useHttpsWithDefaults = UseHttpsWithDefaultsWorker;
+    }
+
+    /// <inheritdoc/>
+    public void ApplyHttpsConfiguration(
+        HttpsConnectionAdapterOptions httpsOptions,
+        EndpointConfig endpoint,
+        KestrelServerOptions serverOptions,
+        CertificateConfig? defaultCertificateConfig,
+        ConfigurationReader configurationReader)
+    {
+        EnsureInitialized(CoreStrings.NeedHttpsConfigurationToApplyHttpsConfiguration);
+        _tlsConfigurationLoader.ApplyHttpsConfiguration(httpsOptions, endpoint, serverOptions, defaultCertificateConfig, configurationReader);
+    }
+
+    /// <inheritdoc/>
+    public ListenOptions UseHttpsWithSni(ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions, EndpointConfig endpoint)
+    {
+        // This doesn't get a distinct string since it won't actually throw - it's always called after ApplyHttpsConfiguration
+        EnsureInitialized(CoreStrings.NeedHttpsConfigurationToApplyHttpsConfiguration);
+        return _tlsConfigurationLoader.UseHttpsWithSni(listenOptions, httpsOptions, endpoint);
+    }
+
+    /// <inheritdoc/>
+    public CertificateAndConfig? LoadDefaultCertificate(ConfigurationReader configurationReader)
+    {
+        EnsureInitialized(CoreStrings.NeedHttpsConfigurationToLoadDefaultCertificate);
+        return _tlsConfigurationLoader.LoadDefaultCertificate(configurationReader);
+    }
+
+    /// <inheritdoc/>
+    public void PopulateMultiplexedTransportFeatures(FeatureCollection features, ListenOptions listenOptions)
+    {
+        EnsureInitialized(CoreStrings.NeedHttpsConfigurationToUseHttp3);
+        _populateMultiplexedTransportFeatures.Invoke(features, listenOptions);
+    }
+
+    /// <inheritdoc/>
+    public ListenOptions UseHttpsWithDefaults(ListenOptions listenOptions)
+    {
+        EnsureInitialized(CoreStrings.NeedHttpsConfigurationToBindHttpsAddresses);
+        return _useHttpsWithDefaults.Invoke(listenOptions);
+    }
+
+    /// <summary>
+    /// If this instance has not been initialized, initialize it if possible and throw otherwise.
+    /// </summary>
+    /// <exception cref="InvalidOperationException">If initialization is not possible.</exception>
+    [MemberNotNull(nameof(_useHttpsWithDefaults), nameof(_tlsConfigurationLoader), nameof(_populateMultiplexedTransportFeatures))]
+    private void EnsureInitialized(string uninitializedError)
+    {
+        if (!_isInitialized)
+        {
+            if (_initializer is not null)
+            {
+                _initializer.Initialize(this);
+            }
+            else
+            {
+                throw new InvalidOperationException(uninitializedError);
+            }
+        }
+
+        Debug.Assert(_useHttpsWithDefaults is not null);
+        Debug.Assert(_tlsConfigurationLoader is not null);
+        Debug.Assert(_populateMultiplexedTransportFeatures is not null);
+    }
+
+    /// <summary>
+    /// The initialized implementation of <see cref="PopulateMultiplexedTransportFeatures"/>.
+    /// </summary>
+    internal static void PopulateMultiplexedTransportFeaturesWorker(FeatureCollection features, ListenOptions listenOptions)
+    {
+        // HttpsOptions or HttpsCallbackOptions should always be set in production, but it's not set for InMemory tests.
+        // The QUIC transport will check if TlsConnectionCallbackOptions is missing.
+        if (listenOptions.HttpsOptions != null)
+        {
+            var sslServerAuthenticationOptions = HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions);
+            features.Set(new TlsConnectionCallbackOptions
+            {
+                ApplicationProtocols = sslServerAuthenticationOptions.ApplicationProtocols ?? new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
+                OnConnection = (context, cancellationToken) => ValueTask.FromResult(sslServerAuthenticationOptions),
+                OnConnectionState = null,
+            });
+        }
+        else if (listenOptions.HttpsCallbackOptions != null)
+        {
+            features.Set(new TlsConnectionCallbackOptions
+            {
+                ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
+                OnConnection = (context, cancellationToken) =>
+                {
+                    return listenOptions.HttpsCallbackOptions.OnConnection(new TlsHandshakeCallbackContext
+                    {
+                        ClientHelloInfo = context.ClientHelloInfo,
+                        CancellationToken = cancellationToken,
+                        State = context.State,
+                        Connection = new ConnectionContextAdapter(context.Connection),
+                    });
+                },
+                OnConnectionState = listenOptions.HttpsCallbackOptions.OnConnectionState,
+            });
+        }
+    }
+
+    /// <summary>
+    /// The initialized implementation of <see cref="UseHttpsWithDefaults"/>.
+    /// </summary>
+    internal static ListenOptions UseHttpsWithDefaultsWorker(ListenOptions listenOptions)
+    {
+        return listenOptions.UseHttps();
+    }
+
+    /// <summary>
+    /// TlsHandshakeCallbackContext.Connection is ConnectionContext but QUIC connection only implements BaseConnectionContext.
+    /// </summary>
+    private sealed class ConnectionContextAdapter : ConnectionContext
+    {
+        private readonly BaseConnectionContext _inner;
+
+        public ConnectionContextAdapter(BaseConnectionContext inner) => _inner = inner;
+
+        public override IDuplexPipe Transport
+        {
+            get => throw new NotSupportedException("Not supported by HTTP/3 connections.");
+            set => throw new NotSupportedException("Not supported by HTTP/3 connections.");
+        }
+        public override string ConnectionId
+        {
+            get => _inner.ConnectionId;
+            set => _inner.ConnectionId = value;
+        }
+        public override IFeatureCollection Features => _inner.Features;
+        public override IDictionary<object, object?> Items
+        {
+            get => _inner.Items;
+            set => _inner.Items = value;
+        }
+        public override EndPoint? LocalEndPoint
+        {
+            get => _inner.LocalEndPoint;
+            set => _inner.LocalEndPoint = value;
+        }
+        public override EndPoint? RemoteEndPoint
+        {
+            get => _inner.RemoteEndPoint;
+            set => _inner.RemoteEndPoint = value;
+        }
+        public override CancellationToken ConnectionClosed
+        {
+            get => _inner.ConnectionClosed;
+            set => _inner.ConnectionClosed = value;
+        }
+        public override ValueTask DisposeAsync() => _inner.DisposeAsync();
+    }
+
+    /// <summary>
+    /// Register an instance of this type to initialize registered instances of <see cref="HttpsConfigurationService"/>.
+    /// </summary>
+    internal interface IInitializer
+    {
+        /// <summary>
+        /// Invokes <see cref="IHttpsConfigurationService.Initialize"/>, passing appropriate arguments.
+        /// </summary>
+        void Initialize(IHttpsConfigurationService httpsConfigurationService);
+    }
+
+    /// <inheritdoc/>
+    internal sealed class Initializer : IInitializer
+    {
+        private readonly IHostEnvironment _hostEnvironment;
+        private readonly ILogger<KestrelServer> _serverLogger;
+        private readonly ILogger<HttpsConnectionMiddleware> _httpsLogger;
+
+        public Initializer(
+            IHostEnvironment hostEnvironment,
+            ILogger<KestrelServer> serverLogger,
+            ILogger<HttpsConnectionMiddleware> httpsLogger)
+        {
+            _hostEnvironment = hostEnvironment;
+            _serverLogger = serverLogger;
+            _httpsLogger = httpsLogger;
+        }
+
+        /// <inheritdoc/>
+        public void Initialize(IHttpsConfigurationService httpsConfigurationService)
+        {
+            httpsConfigurationService.Initialize(_hostEnvironment, _serverLogger, _httpsLogger);
+        }
+    }
+}
+

+ 5 - 0
src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs

@@ -55,6 +55,11 @@ public class HttpsConnectionAdapterOptions
     /// </summary>
     public Func<ConnectionContext?, string?, X509Certificate2?>? ServerCertificateSelector { get; set; }
 
+    /// <summary>
+    /// Convenient shorthand for a common check.
+    /// </summary>
+    internal bool HasServerCertificateOrSelector => ServerCertificate is not null || ServerCertificateSelector is not null;
+
     /// <summary>
     /// Specifies the client certificate requirements for a HTTPS connection. Defaults to <see cref="ClientCertificateMode.NoCertificate"/>.
     /// </summary>

+ 100 - 0
src/Servers/Kestrel/Core/src/IHttpsConfigurationService.cs

@@ -0,0 +1,100 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
+using Microsoft.AspNetCore.Server.Kestrel.Https;
+using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core;
+
+/// <summary>
+/// An abstraction over various things that would prevent us from trimming TLS support in `CreateSlimBuilder`
+/// scenarios.  In normal usage, it will *always* be registered by only be <see cref="IsInitialized"/> if the
+/// consumer explicitly opts into having HTTPS/TLS support.
+/// </summary>
+internal interface IHttpsConfigurationService
+{
+    /// <summary>
+    /// If this property returns false, then methods other than <see cref="Initialize"/> will throw.
+    /// The most obvious way to make this true is to call <see cref="Initialize"/>, but some implementations
+    /// may offer alternative mechanisms.
+    /// </summary>
+    bool IsInitialized { get; }
+
+    /// <summary>
+    /// Replaces the implementations off all other methods with functioning (as opposed to throwing) versions.
+    /// </summary>
+    void Initialize(
+        IHostEnvironment hostEnvironment,
+        ILogger<KestrelServer> serverLogger,
+        ILogger<HttpsConnectionMiddleware> httpsLogger);
+
+    /// <summary>
+    /// Applies various configuration settings to <paramref name="httpsOptions"/> and <paramref name="endpoint"/>.
+    /// </summary>
+    /// <remarks>
+    /// For use during configuration loading (esp in <see cref="KestrelConfigurationLoader"/>).
+    /// </remarks>
+    void ApplyHttpsConfiguration(
+        HttpsConnectionAdapterOptions httpsOptions,
+        EndpointConfig endpoint,
+        KestrelServerOptions serverOptions,
+        CertificateConfig? defaultCertificateConfig,
+        ConfigurationReader configurationReader);
+
+    /// <summary>
+    /// Calls an appropriate overload of <see cref="Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(ListenOptions)"/>
+    /// on <paramref name="listenOptions"/>, with or without SNI, according to how <paramref name="endpoint"/> is configured.
+    /// </summary>
+    /// <returns>Updated <see cref="ListenOptions"/> for convenient chaining.</returns>
+    /// <remarks>
+    /// For use during configuration loading (esp in <see cref="KestrelConfigurationLoader"/>).
+    /// </remarks>
+    ListenOptions UseHttpsWithSni(ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions, EndpointConfig endpoint);
+
+    /// <summary>
+    /// Retrieves the default or, failing that, developer certificate from <paramref name="configurationReader"/>.
+    /// </summary>
+    /// <remarks>
+    /// For use during configuration loading (esp in <see cref="KestrelConfigurationLoader"/>).
+    /// </remarks>
+    CertificateAndConfig? LoadDefaultCertificate(ConfigurationReader configurationReader);
+
+    /// <summary>
+    /// Updates <paramref name="features"/> with multiplexed transport (i.e. HTTP/3) features based on
+    /// the configuration of <paramref name="listenOptions"/>.
+    /// </summary>
+    /// <remarks>
+    /// For use during endpoint binding (esp in <see cref="Internal.Infrastructure.TransportManager"/>).
+    /// </remarks>
+    void PopulateMultiplexedTransportFeatures(FeatureCollection features, ListenOptions listenOptions);
+
+    /// <summary>
+    /// Calls <see cref="Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(ListenOptions)"/>
+    /// on <paramref name="listenOptions"/>.
+    /// </summary>
+    /// <returns>Updated <see cref="ListenOptions"/> for convenient chaining.</returns>
+    /// <remarks>
+    /// For use during address binding (esp in <see cref="AddressBinder"/>).
+    /// </remarks>
+    ListenOptions UseHttpsWithDefaults(ListenOptions listenOptions);
+}
+
+/// <summary>
+/// A <see cref="Certificate"/>-<see cref="CertificateConfig"/> pair.
+/// </summary>
+internal readonly struct CertificateAndConfig
+{
+    public readonly X509Certificate2 Certificate;
+    public readonly CertificateConfig CertificateConfig;
+
+    public CertificateAndConfig(X509Certificate2 certificate, CertificateConfig certificateConfig)
+    {
+        Certificate = certificate;
+        CertificateConfig = certificateConfig;
+    }
+}

+ 13 - 11
src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs

@@ -6,7 +6,6 @@ using System.Linq;
 using System.Net;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Connections;
-using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Hosting.Server.Features;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
@@ -17,12 +16,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
 internal sealed class AddressBinder
 {
     // note this doesn't copy the ListenOptions[], only call this with an array that isn't mutated elsewhere
-    public static async Task BindAsync(ListenOptions[] listenOptions, AddressBindContext context, CancellationToken cancellationToken)
+    public static async Task BindAsync(ListenOptions[] listenOptions, AddressBindContext context, Func<ListenOptions, ListenOptions> useHttps, CancellationToken cancellationToken)
     {
         var strategy = CreateStrategy(
             listenOptions,
             context.Addresses.ToArray(),
-            context.ServerAddressesFeature.PreferHostingUrls);
+            context.ServerAddressesFeature.PreferHostingUrls,
+            useHttps);
 
         // reset options. The actual used options and addresses will be populated
         // by the address binding feature
@@ -32,7 +32,7 @@ internal sealed class AddressBinder
         await strategy.BindAsync(context, cancellationToken).ConfigureAwait(false);
     }
 
-    private static IStrategy CreateStrategy(ListenOptions[] listenOptions, string[] addresses, bool preferAddresses)
+    private static IStrategy CreateStrategy(ListenOptions[] listenOptions, string[] addresses, bool preferAddresses, Func<ListenOptions, ListenOptions> useHttps)
     {
         var hasListenOptions = listenOptions.Length > 0;
         var hasAddresses = addresses.Length > 0;
@@ -41,10 +41,10 @@ internal sealed class AddressBinder
         {
             if (hasListenOptions)
             {
-                return new OverrideWithAddressesStrategy(addresses);
+                return new OverrideWithAddressesStrategy(addresses, useHttps);
             }
 
-            return new AddressesStrategy(addresses);
+            return new AddressesStrategy(addresses, useHttps);
         }
         else if (hasListenOptions)
         {
@@ -58,7 +58,7 @@ internal sealed class AddressBinder
         else if (hasAddresses)
         {
             // If no endpoints are configured directly using KestrelServerOptions, use those configured via the IServerAddressesFeature.
-            return new AddressesStrategy(addresses);
+            return new AddressesStrategy(addresses, useHttps);
         }
         else
         {
@@ -162,8 +162,8 @@ internal sealed class AddressBinder
 
     private sealed class OverrideWithAddressesStrategy : AddressesStrategy
     {
-        public OverrideWithAddressesStrategy(IReadOnlyCollection<string> addresses)
-            : base(addresses)
+        public OverrideWithAddressesStrategy(IReadOnlyCollection<string> addresses, Func<ListenOptions, ListenOptions> useHttps)
+            : base(addresses, useHttps)
         {
         }
 
@@ -216,10 +216,12 @@ internal sealed class AddressBinder
     private class AddressesStrategy : IStrategy
     {
         protected readonly IReadOnlyCollection<string> _addresses;
+        private readonly Func<ListenOptions, ListenOptions> _useHttps;
 
-        public AddressesStrategy(IReadOnlyCollection<string> addresses)
+        public AddressesStrategy(IReadOnlyCollection<string> addresses, Func<ListenOptions, ListenOptions> useHttps)
         {
             _addresses = addresses;
+            _useHttps = useHttps;
         }
 
         public virtual async Task BindAsync(AddressBindContext context, CancellationToken cancellationToken)
@@ -231,7 +233,7 @@ internal sealed class AddressBinder
 
                 if (https && !options.IsTls)
                 {
-                    options.UseHttps();
+                    _useHttps(options);
                 }
 
                 await options.BindAsync(context, cancellationToken).ConfigureAwait(false);

+ 5 - 77
src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs

@@ -3,13 +3,9 @@
 
 #nullable enable
 
-using System.IO.Pipelines;
 using System.Net;
-using System.Net.Security;
 using Microsoft.AspNetCore.Connections;
 using Microsoft.AspNetCore.Http.Features;
-using Microsoft.AspNetCore.Server.Kestrel.Https;
-using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
 
 namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
 
@@ -19,15 +15,18 @@ internal sealed class TransportManager
 
     private readonly List<IConnectionListenerFactory> _transportFactories;
     private readonly List<IMultiplexedConnectionListenerFactory> _multiplexedTransportFactories;
+    private readonly IHttpsConfigurationService _httpsConfigurationService;
     private readonly ServiceContext _serviceContext;
 
     public TransportManager(
         List<IConnectionListenerFactory> transportFactories,
         List<IMultiplexedConnectionListenerFactory> multiplexedTransportFactories,
+        IHttpsConfigurationService httpsConfigurationService,
         ServiceContext serviceContext)
     {
         _transportFactories = transportFactories;
         _multiplexedTransportFactories = multiplexedTransportFactories;
+        _httpsConfigurationService = httpsConfigurationService;
         _serviceContext = serviceContext;
     }
 
@@ -72,36 +71,8 @@ internal sealed class TransportManager
 
         var features = new FeatureCollection();
 
-        // HttpsOptions or HttpsCallbackOptions should always be set in production, but it's not set for InMemory tests.
-        // The QUIC transport will check if TlsConnectionCallbackOptions is missing.
-        if (listenOptions.HttpsOptions != null)
-        {
-            var sslServerAuthenticationOptions = HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions);
-            features.Set(new TlsConnectionCallbackOptions
-            {
-                ApplicationProtocols = sslServerAuthenticationOptions.ApplicationProtocols ?? new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
-                OnConnection = (context, cancellationToken) => ValueTask.FromResult(sslServerAuthenticationOptions),
-                OnConnectionState = null,
-            });
-        }
-        else if (listenOptions.HttpsCallbackOptions != null)
-        {
-            features.Set(new TlsConnectionCallbackOptions
-            {
-                ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
-                OnConnection = (context, cancellationToken) =>
-                {
-                    return listenOptions.HttpsCallbackOptions.OnConnection(new TlsHandshakeCallbackContext
-                    {
-                        ClientHelloInfo = context.ClientHelloInfo,
-                        CancellationToken = cancellationToken,
-                        State = context.State,
-                        Connection = new ConnectionContextAdapter(context.Connection),
-                    });
-                },
-                OnConnectionState = listenOptions.HttpsCallbackOptions.OnConnectionState,
-            });
-        }
+        // Will throw an appropriate error if it's not enabled
+        _httpsConfigurationService.PopulateMultiplexedTransportFeatures(features, listenOptions);
 
         foreach (var multiplexedTransportFactory in _multiplexedTransportFactories)
         {
@@ -124,49 +95,6 @@ internal sealed class TransportManager
         return selector?.CanBind(endPoint) ?? true;
     }
 
-    /// <summary>
-    /// TlsHandshakeCallbackContext.Connection is ConnectionContext but QUIC connection only implements BaseConnectionContext.
-    /// </summary>
-    private sealed class ConnectionContextAdapter : ConnectionContext
-    {
-        private readonly BaseConnectionContext _inner;
-
-        public ConnectionContextAdapter(BaseConnectionContext inner) => _inner = inner;
-
-        public override IDuplexPipe Transport
-        {
-            get => throw new NotSupportedException("Not supported by HTTP/3 connections.");
-            set => throw new NotSupportedException("Not supported by HTTP/3 connections.");
-        }
-        public override string ConnectionId
-        {
-            get => _inner.ConnectionId;
-            set => _inner.ConnectionId = value;
-        }
-        public override IFeatureCollection Features => _inner.Features;
-        public override IDictionary<object, object?> Items
-        {
-            get => _inner.Items;
-            set => _inner.Items = value;
-        }
-        public override EndPoint? LocalEndPoint
-        {
-            get => _inner.LocalEndPoint;
-            set => _inner.LocalEndPoint = value;
-        }
-        public override EndPoint? RemoteEndPoint
-        {
-            get => _inner.RemoteEndPoint;
-            set => _inner.RemoteEndPoint = value;
-        }
-        public override CancellationToken ConnectionClosed
-        {
-            get => _inner.ConnectionClosed;
-            set => _inner.ConnectionClosed = value;
-        }
-        public override ValueTask DisposeAsync() => _inner.DisposeAsync();
-    }
-
     private void StartAcceptLoop<T>(IConnectionListener<T> connectionListener, Func<T, Task> connectionDelegate, EndpointConfig? endpointConfig) where T : BaseConnectionContext
     {
         var transportConnectionManager = new TransportConnectionManager(_serviceContext.ConnectionManager);

+ 8 - 4
src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs

@@ -23,6 +23,7 @@ internal sealed class KestrelServerImpl : IServer
     private readonly TransportManager _transportManager;
     private readonly List<IConnectionListenerFactory> _transportFactories;
     private readonly List<IMultiplexedConnectionListenerFactory> _multiplexedTransportFactories;
+    private readonly IHttpsConfigurationService _httpsConfigurationService;
 
     private readonly SemaphoreSlim _bindSemaphore = new SemaphoreSlim(initialCount: 1);
     private bool _hasStarted;
@@ -36,9 +37,10 @@ internal sealed class KestrelServerImpl : IServer
         IOptions<KestrelServerOptions> options,
         IEnumerable<IConnectionListenerFactory> transportFactories,
         IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories,
+        IHttpsConfigurationService httpsConfigurationService,
         ILoggerFactory loggerFactory,
         KestrelMetrics metrics)
-        : this(transportFactories, multiplexedFactories, CreateServiceContext(options, loggerFactory, diagnosticSource: null, metrics))
+        : this(transportFactories, multiplexedFactories, httpsConfigurationService, CreateServiceContext(options, loggerFactory, diagnosticSource: null, metrics))
     {
     }
 
@@ -47,12 +49,14 @@ internal sealed class KestrelServerImpl : IServer
     internal KestrelServerImpl(
         IEnumerable<IConnectionListenerFactory> transportFactories,
         IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories,
+        IHttpsConfigurationService httpsConfigurationService,
         ServiceContext serviceContext)
     {
         ArgumentNullException.ThrowIfNull(transportFactories);
 
         _transportFactories = transportFactories.Reverse().ToList();
         _multiplexedTransportFactories = multiplexedFactories.Reverse().ToList();
+        _httpsConfigurationService = httpsConfigurationService;
 
         if (_transportFactories.Count == 0 && _multiplexedTransportFactories.Count == 0)
         {
@@ -65,7 +69,7 @@ internal sealed class KestrelServerImpl : IServer
         _serverAddresses = new ServerAddressesFeature();
         Features.Set<IServerAddressesFeature>(_serverAddresses);
 
-        _transportManager = new TransportManager(_transportFactories, _multiplexedTransportFactories, ServiceContext);
+        _transportManager = new TransportManager(_transportFactories, _multiplexedTransportFactories, _httpsConfigurationService, ServiceContext);
     }
 
     private static ServiceContext CreateServiceContext(IOptions<KestrelServerOptions> options, ILoggerFactory loggerFactory, DiagnosticSource? diagnosticSource, KestrelMetrics metrics)
@@ -168,7 +172,7 @@ internal sealed class KestrelServerImpl : IServer
                 // Quic isn't registered if it's not supported, throw if we can't fall back to 1 or 2
                 if (hasHttp3 && _multiplexedTransportFactories.Count == 0 && !(hasHttp1 || hasHttp2))
                 {
-                    throw new InvalidOperationException("This platform doesn't support QUIC or HTTP/3.");
+                    throw new InvalidOperationException("Unable to bind an HTTP/3 endpoint. This could be because QUIC has not been configured using UseQuic, or the platform doesn't support QUIC or HTTP/3.");
                 }
 
                 // Disable adding alt-svc header if endpoint has configured not to or there is no
@@ -302,7 +306,7 @@ internal sealed class KestrelServerImpl : IServer
 
             Options.ConfigurationLoader?.Load();
 
-            await AddressBinder.BindAsync(Options.GetListenOptions(), AddressBindContext!, cancellationToken).ConfigureAwait(false);
+            await AddressBinder.BindAsync(Options.GetListenOptions(), AddressBindContext!, _httpsConfigurationService.UseHttpsWithDefaults, cancellationToken).ConfigureAwait(false);
             _configChangedRegistration = reloadToken?.RegisterChangeCallback(TriggerRebind, this);
         }
         finally

+ 1 - 1
src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs

@@ -54,7 +54,7 @@ internal sealed class SniOptionsSelector
 
             if (sslOptions.ServerCertificate is null)
             {
-                if (fallbackHttpsOptions.ServerCertificate is null && _fallbackServerCertificateSelector is null)
+                if (!fallbackHttpsOptions.HasServerCertificateOrSelector)
                 {
                     throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
                 }

+ 20 - 167
src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs

@@ -1,21 +1,13 @@
 // 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.CodeAnalysis;
 using System.Linq;
 using System.Net;
-using System.Security.Cryptography;
 using System.Security.Cryptography.X509Certificates;
-using Microsoft.AspNetCore.Certificates.Generation;
-using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Server.Kestrel.Core;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
-using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates;
 using Microsoft.AspNetCore.Server.Kestrel.Https;
-using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
 using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
 
 namespace Microsoft.AspNetCore.Server.Kestrel;
 
@@ -24,26 +16,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel;
 /// </summary>
 public class KestrelConfigurationLoader
 {
+    private readonly IHttpsConfigurationService _httpsConfigurationService;
+
     private bool _loaded;
 
     internal KestrelConfigurationLoader(
         KestrelServerOptions options,
         IConfiguration configuration,
-        IHostEnvironment hostEnvironment,
-        bool reloadOnChange,
-        ILogger<KestrelServer> logger,
-        ILogger<HttpsConnectionMiddleware> httpsLogger)
+        IHttpsConfigurationService httpsConfigurationService,
+        bool reloadOnChange)
     {
-        Options = options ?? throw new ArgumentNullException(nameof(options));
-        Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
-        HostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment));
-        Logger = logger ?? throw new ArgumentNullException(nameof(logger));
-        HttpsLogger = httpsLogger ?? throw new ArgumentNullException(nameof(logger));
+        Options = options;
+        Configuration = configuration;
 
         ReloadOnChange = reloadOnChange;
 
         ConfigurationReader = new ConfigurationReader(configuration);
-        CertificateConfigLoader = new CertificateConfigLoader(hostEnvironment, logger);
+
+        _httpsConfigurationService = httpsConfigurationService;
     }
 
     /// <summary>
@@ -62,14 +52,8 @@ public class KestrelConfigurationLoader
     /// </summary>
     internal bool ReloadOnChange { get; }
 
-    private IHostEnvironment HostEnvironment { get; }
-    private ILogger<KestrelServer> Logger { get; }
-    private ILogger<HttpsConnectionMiddleware> HttpsLogger { get; }
-
     private ConfigurationReader ConfigurationReader { get; set; }
 
-    private ICertificateConfigLoader CertificateConfigLoader { get; }
-
     private IDictionary<string, Action<EndpointConfiguration>> EndpointConfigurations { get; }
         = new Dictionary<string, Action<EndpointConfiguration>>(0, StringComparer.OrdinalIgnoreCase);
 
@@ -278,7 +262,11 @@ public class KestrelConfigurationLoader
 
         ConfigurationReader = new ConfigurationReader(Configuration);
 
-        LoadDefaultCert();
+        if (_httpsConfigurationService.IsInitialized && _httpsConfigurationService.LoadDefaultCertificate(ConfigurationReader) is CertificateAndConfig certPair)
+        {
+            DefaultCertificate = certPair.Certificate;
+            DefaultCertificateConfig = certPair.CertificateConfig;
+        }
 
         foreach (var endpoint in ConfigurationReader.Endpoints)
         {
@@ -307,42 +295,8 @@ public class KestrelConfigurationLoader
 
             if (https)
             {
-                // Defaults
-                Options.ApplyHttpsDefaults(httpsOptions);
-
-                if (endpoint.SslProtocols.HasValue)
-                {
-                    httpsOptions.SslProtocols = endpoint.SslProtocols.Value;
-                }
-                else
-                {
-                    // Ensure endpoint is reloaded if it used the default protocol and the SslProtocols changed.
-                    endpoint.SslProtocols = ConfigurationReader.EndpointDefaults.SslProtocols;
-                }
-
-                if (endpoint.ClientCertificateMode.HasValue)
-                {
-                    httpsOptions.ClientCertificateMode = endpoint.ClientCertificateMode.Value;
-                }
-                else
-                {
-                    // Ensure endpoint is reloaded if it used the default mode and the ClientCertificateMode changed.
-                    endpoint.ClientCertificateMode = ConfigurationReader.EndpointDefaults.ClientCertificateMode;
-                }
-
-                // A cert specified directly on the endpoint overrides any defaults.
-                var (serverCert, fullChain) = CertificateConfigLoader.LoadCertificate(endpoint.Certificate, endpoint.Name);
-                httpsOptions.ServerCertificate = serverCert ?? httpsOptions.ServerCertificate;
-                httpsOptions.ServerCertificateChain = fullChain ?? httpsOptions.ServerCertificateChain;
-
-                if (httpsOptions.ServerCertificate == null && httpsOptions.ServerCertificateSelector == null)
-                {
-                    // Fallback
-                    Options.ApplyDefaultCertificate(httpsOptions);
-
-                    // Ensure endpoint is reloaded if it used the default certificate and the certificate changed.
-                    endpoint.Certificate = DefaultCertificateConfig;
-                }
+                // Throws an appropriate exception if https configuration isn't enabled
+                _httpsConfigurationService.ApplyHttpsConfiguration(httpsOptions, endpoint, Options, DefaultCertificateConfig, ConfigurationReader);
             }
 
             // Now that defaults have been loaded, we can compare to the currently bound endpoints to see if the config changed.
@@ -370,30 +324,12 @@ public class KestrelConfigurationLoader
             }
 
             // EndpointDefaults or configureEndpoint may have added an https adapter.
-            if (https && !listenOptions.IsTls)
+            if (https)
             {
-                if (endpoint.Sni.Count == 0)
-                {
-                    if (httpsOptions.ServerCertificate == null && httpsOptions.ServerCertificateSelector == null)
-                    {
-                        throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
-                    }
-
-                    listenOptions.UseHttps(httpsOptions);
-                }
-                else
-                {
-                    var sniOptionsSelector = new SniOptionsSelector(endpoint.Name, endpoint.Sni, CertificateConfigLoader,
-                        httpsOptions, listenOptions.Protocols, HttpsLogger);
-                    var tlsCallbackOptions = new TlsHandshakeCallbackOptions()
-                    {
-                        OnConnection = SniOptionsSelector.OptionsCallback,
-                        HandshakeTimeout = httpsOptions.HandshakeTimeout,
-                        OnConnectionState = sniOptionsSelector,
-                    };
-
-                    listenOptions.UseHttps(tlsCallbackOptions);
-                }
+                // This would throw if it were invoked without https configuration having been enabled,
+                // but that won't happen because ApplyHttpsConfiguration would throw above under those
+                // circumstances.
+                _httpsConfigurationService.UseHttpsWithSni(listenOptions, httpsOptions, endpoint);
             }
 
             listenOptions.EndpointConfig = endpoint;
@@ -411,87 +347,4 @@ public class KestrelConfigurationLoader
 
         return (endpointsToStop, endpointsToStart);
     }
-
-    private void LoadDefaultCert()
-    {
-        if (ConfigurationReader.Certificates.TryGetValue("Default", out var defaultCertConfig))
-        {
-            var (defaultCert, _ /* cert chain */) = CertificateConfigLoader.LoadCertificate(defaultCertConfig, "Default");
-            if (defaultCert != null)
-            {
-                DefaultCertificateConfig = defaultCertConfig;
-                DefaultCertificate = defaultCert;
-            }
-        }
-        else
-        {
-            var (certificate, certificateConfig) = FindDeveloperCertificateFile();
-            if (certificate != null)
-            {
-                Logger.LocatedDevelopmentCertificate(certificate);
-                DefaultCertificateConfig = certificateConfig;
-                DefaultCertificate = certificate;
-            }
-        }
-    }
-
-    private (X509Certificate2?, CertificateConfig?) FindDeveloperCertificateFile()
-    {
-        string? certificatePath = null;
-        if (ConfigurationReader.Certificates.TryGetValue("Development", out var certificateConfig) &&
-            certificateConfig.Path == null &&
-            certificateConfig.Password != null &&
-            TryGetCertificatePath(out certificatePath) &&
-            File.Exists(certificatePath))
-        {
-            try
-            {
-                var certificate = new X509Certificate2(certificatePath, certificateConfig.Password);
-
-                if (IsDevelopmentCertificate(certificate))
-                {
-                    return (certificate, certificateConfig);
-                }
-            }
-            catch (CryptographicException)
-            {
-                Logger.FailedToLoadDevelopmentCertificate(certificatePath);
-            }
-        }
-        else if (!string.IsNullOrEmpty(certificatePath))
-        {
-            Logger.FailedToLocateDevelopmentCertificateFile(certificatePath);
-        }
-
-        return (null, null);
-    }
-
-    private static bool IsDevelopmentCertificate(X509Certificate2 certificate)
-    {
-        if (!string.Equals(certificate.Subject, "CN=localhost", StringComparison.Ordinal))
-        {
-            return false;
-        }
-
-        foreach (var ext in certificate.Extensions)
-        {
-            if (string.Equals(ext.Oid?.Value, CertificateManager.AspNetHttpsOid, StringComparison.Ordinal))
-            {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    private bool TryGetCertificatePath([NotNullWhen(true)] out string? path)
-    {
-        // See https://github.com/aspnet/Hosting/issues/1294
-        var appData = Environment.GetEnvironmentVariable("APPDATA");
-        var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
-        var basePath = appData != null ? Path.Combine(appData, "ASP.NET", "https") : null;
-        basePath = basePath ?? (home != null ? Path.Combine(home, ".aspnet", "https") : null);
-        path = basePath != null ? Path.Combine(basePath, $"{HostEnvironment.ApplicationName}.pfx") : null;
-        return path != null;
-    }
 }

+ 45 - 0
src/Servers/Kestrel/Core/src/KestrelServer.cs

@@ -5,7 +5,11 @@ using System.Diagnostics.Metrics;
 using Microsoft.AspNetCore.Connections;
 using Microsoft.AspNetCore.Hosting.Server;
 using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
+using Microsoft.AspNetCore.Server.Kestrel.Https;
+using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
+using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Metrics;
 using Microsoft.Extensions.Options;
@@ -31,6 +35,7 @@ public class KestrelServer : IServer
             options,
             new[] { transportFactory ?? throw new ArgumentNullException(nameof(transportFactory)) },
             Array.Empty<IMultiplexedConnectionListenerFactory>(),
+            new SimpleHttpsConfigurationService(),
             loggerFactory,
             new KestrelMetrics(new DummyMeterFactory()));
     }
@@ -70,4 +75,44 @@ public class KestrelServer : IServer
 
         public Meter CreateMeter(MeterOptions options) => new Meter(options.Name, options.Version);
     }
+
+    private sealed class SimpleHttpsConfigurationService : IHttpsConfigurationService
+    {
+        public bool IsInitialized => true;
+
+        public void Initialize(IHostEnvironment hostEnvironment, ILogger<KestrelServer> serverLogger, ILogger<HttpsConnectionMiddleware> httpsLogger)
+        {
+            // Already initialized
+        }
+
+        public void PopulateMultiplexedTransportFeatures(FeatureCollection features, ListenOptions listenOptions)
+        {
+            HttpsConfigurationService.PopulateMultiplexedTransportFeaturesWorker(features, listenOptions);
+        }
+
+        public ListenOptions UseHttpsWithDefaults(ListenOptions listenOptions)
+        {
+            return HttpsConfigurationService.UseHttpsWithDefaultsWorker(listenOptions);
+        }
+
+        public void ApplyHttpsConfiguration(
+            HttpsConnectionAdapterOptions httpsOptions,
+            EndpointConfig endpoint,
+            KestrelServerOptions serverOptions,
+            CertificateConfig? defaultCertificateConfig,
+            ConfigurationReader configurationReader)
+        {
+            throw new NotImplementedException(); // Not actually required by this impl
+        }
+
+        public ListenOptions UseHttpsWithSni(ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions, EndpointConfig endpoint)
+        {
+            throw new NotImplementedException(); // Not actually required by this impl
+        }
+
+        public CertificateAndConfig? LoadDefaultCertificate(ConfigurationReader configurationReader)
+        {
+            throw new NotImplementedException(); // Not actually required by this impl
+        }
+    }
 }

+ 20 - 6
src/Servers/Kestrel/Core/src/KestrelServerOptions.cs

@@ -250,11 +250,15 @@ public class KestrelServerOptions
 
     internal void ApplyDefaultCertificate(HttpsConnectionAdapterOptions httpsOptions)
     {
-        if (httpsOptions.ServerCertificate != null || httpsOptions.ServerCertificateSelector != null)
+        if (httpsOptions.HasServerCertificateOrSelector)
         {
             return;
         }
 
+        // It's important (and currently true) that we don't reach here with https configuration uninitialized because
+        // we might incorrectly favor the development certificate over one specified by the user.
+        Debug.Assert(ApplicationServices.GetRequiredService<IHttpsConfigurationService>().IsInitialized, "HTTPS configuration should have been enabled");
+
         if (TestOverrideDefaultCertificate is X509Certificate2 certificateFromTest)
         {
             httpsOptions.ServerCertificate = certificateFromTest;
@@ -278,6 +282,19 @@ public class KestrelServerOptions
         httpsOptions.ServerCertificate = DevelopmentCertificate;
     }
 
+    internal void EnableHttpsConfiguration()
+    {
+        var httpsConfigurationService = ApplicationServices.GetRequiredService<IHttpsConfigurationService>();
+
+        if (!httpsConfigurationService.IsInitialized)
+        {
+            var hostEnvironment = ApplicationServices.GetRequiredService<IHostEnvironment>();
+            var logger = ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
+            var httpsLogger = ApplicationServices.GetRequiredService<ILogger<HttpsConnectionMiddleware>>();
+            httpsConfigurationService.Initialize(hostEnvironment, logger, httpsLogger);
+        }
+    }
+
     internal void Serialize(Utf8JsonWriter writer)
     {
         writer.WritePropertyName(nameof(AllowSynchronousIO));
@@ -392,11 +409,8 @@ public class KestrelServerOptions
             throw new InvalidOperationException($"{nameof(ApplicationServices)} must not be null. This is normally set automatically via {nameof(IConfigureOptions<KestrelServerOptions>)}.");
         }
 
-        var hostEnvironment = ApplicationServices.GetRequiredService<IHostEnvironment>();
-        var logger = ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
-        var httpsLogger = ApplicationServices.GetRequiredService<ILogger<HttpsConnectionMiddleware>>();
-
-        var loader = new KestrelConfigurationLoader(this, config, hostEnvironment, reloadOnChange, logger, httpsLogger);
+        var httpsConfigurationService = ApplicationServices.GetRequiredService<IHttpsConfigurationService>();
+        var loader = new KestrelConfigurationLoader(this, config, httpsConfigurationService, reloadOnChange);
         ConfigurationLoader = loader;
         return loader;
     }

+ 4 - 17
src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs

@@ -163,12 +163,15 @@ public static class ListenOptionsHttpsExtensions
     {
         ArgumentNullException.ThrowIfNull(configureOptions);
 
+        // We consider calls to `UseHttps` to be a clear expression of user intent to pull in HTTPS configuration support
+        listenOptions.KestrelServerOptions.EnableHttpsConfiguration();
+
         var options = new HttpsConnectionAdapterOptions();
         listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options);
         configureOptions(options);
         listenOptions.KestrelServerOptions.ApplyDefaultCertificate(options);
 
-        if (options.ServerCertificate == null && options.ServerCertificateSelector == null)
+        if (!options.HasServerCertificateOrSelector)
         {
             throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
         }
@@ -176,22 +179,6 @@ public static class ListenOptionsHttpsExtensions
         return listenOptions.UseHttps(options);
     }
 
-    // Use Https if a default cert is available
-    internal static bool TryUseHttps(this ListenOptions listenOptions)
-    {
-        var options = new HttpsConnectionAdapterOptions();
-        listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options);
-        listenOptions.KestrelServerOptions.ApplyDefaultCertificate(options);
-
-        if (options.ServerCertificate == null && options.ServerCertificateSelector == null)
-        {
-            return false;
-        }
-
-        listenOptions.UseHttps(options);
-        return true;
-    }
-
     /// <summary>
     /// Configure Kestrel to use HTTPS. This does not use default certificates or other defaults specified via config or
     /// <see cref="KestrelServerOptions.ConfigureHttpsDefaults(Action{HttpsConnectionAdapterOptions})"/>.

+ 1 - 1
src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs

@@ -57,7 +57,7 @@ internal sealed class HttpsConnectionMiddleware
     {
         ArgumentNullException.ThrowIfNull(options);
 
-        if (options.ServerCertificate == null && options.ServerCertificateSelector == null)
+        if (!options.HasServerCertificateOrSelector)
         {
             throw new ArgumentException(CoreStrings.ServerCertificateRequired, nameof(options));
         }

+ 205 - 0
src/Servers/Kestrel/Core/src/TlsConfigurationLoader.cs

@@ -0,0 +1,205 @@
+// 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.CodeAnalysis;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Certificates.Generation;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates;
+using Microsoft.AspNetCore.Server.Kestrel.Https;
+using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core;
+
+/// <summary>
+/// An abstraction over the parts of <see cref="KestrelConfigurationLoader"/> that would prevent us from trimming TLS support
+/// in `CreateSlimBuilder` scenarios.  Managed by <see cref="HttpsConfigurationService"/>.
+/// </summary>
+internal sealed class TlsConfigurationLoader
+{
+    private readonly ICertificateConfigLoader _certificateConfigLoader;
+    private readonly string _applicationName;
+    private readonly ILogger<KestrelServer> _serverLogger;
+    private readonly ILogger<HttpsConnectionMiddleware> _httpsLogger;
+
+    public TlsConfigurationLoader(
+        IHostEnvironment hostEnvironment,
+        ILogger<KestrelServer> serverLogger,
+        ILogger<HttpsConnectionMiddleware> httpsLogger)
+    {
+        _certificateConfigLoader = new CertificateConfigLoader(hostEnvironment, serverLogger);
+        _applicationName = hostEnvironment.ApplicationName;
+        _serverLogger = serverLogger;
+        _httpsLogger = httpsLogger;
+    }
+
+    /// <summary>
+    /// Applies various configuration settings to <paramref name="httpsOptions"/> and <paramref name="endpoint"/>.
+    /// </summary>
+    public void ApplyHttpsConfiguration(
+        HttpsConnectionAdapterOptions httpsOptions,
+        EndpointConfig endpoint,
+        KestrelServerOptions serverOptions,
+        CertificateConfig? defaultCertificateConfig,
+        ConfigurationReader configurationReader)
+    {
+        serverOptions.ApplyHttpsDefaults(httpsOptions);
+
+        if (endpoint.SslProtocols.HasValue)
+        {
+            httpsOptions.SslProtocols = endpoint.SslProtocols.Value;
+        }
+        else
+        {
+            // Ensure endpoint is reloaded if it used the default protocol and the SslProtocols changed.
+            endpoint.SslProtocols = configurationReader.EndpointDefaults.SslProtocols;
+        }
+
+        if (endpoint.ClientCertificateMode.HasValue)
+        {
+            httpsOptions.ClientCertificateMode = endpoint.ClientCertificateMode.Value;
+        }
+        else
+        {
+            // Ensure endpoint is reloaded if it used the default mode and the ClientCertificateMode changed.
+            endpoint.ClientCertificateMode = configurationReader.EndpointDefaults.ClientCertificateMode;
+        }
+
+        // A cert specified directly on the endpoint overrides any defaults.
+        var (serverCert, fullChain) = _certificateConfigLoader.LoadCertificate(endpoint.Certificate, endpoint.Name);
+        httpsOptions.ServerCertificate = serverCert ?? httpsOptions.ServerCertificate;
+        httpsOptions.ServerCertificateChain = fullChain ?? httpsOptions.ServerCertificateChain;
+
+        if (!httpsOptions.HasServerCertificateOrSelector)
+        {
+            // Fallback
+            serverOptions.ApplyDefaultCertificate(httpsOptions);
+
+            // Ensure endpoint is reloaded if it used the default certificate and the certificate changed.
+            endpoint.Certificate = defaultCertificateConfig;
+        }
+    }
+
+    /// <summary>
+    /// Calls an appropriate overload of <see cref="Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(ListenOptions)"/>
+    /// on <paramref name="listenOptions"/>, with or without SNI, according to how <paramref name="endpoint"/> is configured.
+    /// </summary>
+    /// <returns>Updated <see cref="ListenOptions"/> for convenient chaining.</returns>
+    public ListenOptions UseHttpsWithSni(
+        ListenOptions listenOptions,
+        HttpsConnectionAdapterOptions httpsOptions,
+        EndpointConfig endpoint)
+    {
+        if (listenOptions.IsTls)
+        {
+            return listenOptions;
+        }
+
+        if (endpoint.Sni.Count == 0)
+        {
+            if (!httpsOptions.HasServerCertificateOrSelector)
+            {
+                throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
+            }
+
+            return listenOptions.UseHttps(httpsOptions);
+        }
+
+        var sniOptionsSelector = new SniOptionsSelector(endpoint.Name, endpoint.Sni, _certificateConfigLoader,
+            httpsOptions, listenOptions.Protocols, _httpsLogger);
+        var tlsCallbackOptions = new TlsHandshakeCallbackOptions()
+        {
+            OnConnection = SniOptionsSelector.OptionsCallback,
+            HandshakeTimeout = httpsOptions.HandshakeTimeout,
+            OnConnectionState = sniOptionsSelector,
+        };
+
+        return listenOptions.UseHttps(tlsCallbackOptions);
+    }
+
+    /// <summary>
+    /// Retrieves the default or, failing that, developer certificate from <paramref name="configurationReader"/>.
+    /// </summary>
+    public CertificateAndConfig? LoadDefaultCertificate(ConfigurationReader configurationReader)
+    {
+        if (configurationReader.Certificates.TryGetValue("Default", out var defaultCertConfig))
+        {
+            var (defaultCert, _ /* cert chain */) = _certificateConfigLoader.LoadCertificate(defaultCertConfig, "Default");
+            if (defaultCert != null)
+            {
+                return new CertificateAndConfig(defaultCert, defaultCertConfig);
+            }
+        }
+        else if (FindDeveloperCertificateFile(configurationReader) is CertificateAndConfig pair)
+        {
+            _serverLogger.LocatedDevelopmentCertificate(pair.Certificate);
+            return pair;
+        }
+
+        return null;
+    }
+
+    private CertificateAndConfig? FindDeveloperCertificateFile(ConfigurationReader configurationReader)
+    {
+        string? certificatePath = null;
+        if (configurationReader.Certificates.TryGetValue("Development", out var certificateConfig) &&
+            certificateConfig.Path == null &&
+            certificateConfig.Password != null &&
+            TryGetCertificatePath(_applicationName, out certificatePath) &&
+            File.Exists(certificatePath))
+        {
+            try
+            {
+                var certificate = new X509Certificate2(certificatePath, certificateConfig.Password);
+
+                if (IsDevelopmentCertificate(certificate))
+                {
+                    return new CertificateAndConfig(certificate, certificateConfig);
+                }
+            }
+            catch (CryptographicException)
+            {
+                _serverLogger.FailedToLoadDevelopmentCertificate(certificatePath);
+            }
+        }
+        else if (!string.IsNullOrEmpty(certificatePath))
+        {
+            _serverLogger.FailedToLocateDevelopmentCertificateFile(certificatePath);
+        }
+
+        return null;
+    }
+
+    private static bool IsDevelopmentCertificate(X509Certificate2 certificate)
+    {
+        if (!string.Equals(certificate.Subject, "CN=localhost", StringComparison.Ordinal))
+        {
+            return false;
+        }
+
+        foreach (var ext in certificate.Extensions)
+        {
+            if (string.Equals(ext.Oid?.Value, CertificateManager.AspNetHttpsOid, StringComparison.Ordinal))
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private static bool TryGetCertificatePath(string applicationName, [NotNullWhen(true)] out string? path)
+    {
+        // See https://github.com/aspnet/Hosting/issues/1294
+        var appData = Environment.GetEnvironmentVariable("APPDATA");
+        var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+        var basePath = appData != null ? Path.Combine(appData, "ASP.NET", "https") : null;
+        basePath = basePath ?? (home != null ? Path.Combine(home, ".aspnet", "https") : null);
+        path = basePath != null ? Path.Combine(basePath, $"{applicationName}.pfx") : null;
+        return path != null;
+    }
+}

+ 8 - 14
src/Servers/Kestrel/Core/test/AddressBinderTests.cs

@@ -1,28 +1,22 @@
 // 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.Collections.Generic;
-using System.IO;
 using System.Net;
 using System.Net.Sockets;
-using System.Threading;
-using System.Threading.Tasks;
 using Microsoft.AspNetCore.Connections;
-using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
-using Microsoft.AspNetCore.Server.Kestrel.Https;
 using Microsoft.AspNetCore.Testing;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
-using Xunit;
 
 namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests;
 
 public class AddressBinderTests
 {
+    private readonly Func<ListenOptions, ListenOptions> _noopUseHttps = l => l;
+
     [Theory]
     [InlineData("http://10.10.10.10:5000/", "10.10.10.10", 5000)]
     [InlineData("http://[::1]:5000", "::1", 5000)]
@@ -172,7 +166,7 @@ public class AddressBinderTests
             endpoint => throw new AddressInUseException("already in use"));
 
         await Assert.ThrowsAsync<IOException>(() =>
-            AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, CancellationToken.None));
+            AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, _noopUseHttps, CancellationToken.None));
     }
 
     [Fact]
@@ -193,7 +187,7 @@ public class AddressBinderTests
             logger,
             endpoint => Task.CompletedTask);
 
-        var bindTask = AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, CancellationToken.None);
+        var bindTask = AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, _noopUseHttps, CancellationToken.None);
         Assert.True(bindTask.IsCompletedSuccessfully);
 
         var log = Assert.Single(logger.Messages);
@@ -221,7 +215,7 @@ public class AddressBinderTests
 
         addressBindContext.ServerAddressesFeature.PreferHostingUrls = true;
 
-        var bindTask = AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, CancellationToken.None);
+        var bindTask = AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, _noopUseHttps, CancellationToken.None);
         Assert.True(bindTask.IsCompletedSuccessfully);
 
         var log = Assert.Single(logger.Messages);
@@ -247,7 +241,7 @@ public class AddressBinderTests
             });
 
         await Assert.ThrowsAsync<OperationCanceledException>(() =>
-            AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, new CancellationToken(true)));
+            AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, _noopUseHttps, new CancellationToken(true)));
     }
 
     [Theory]
@@ -284,7 +278,7 @@ public class AddressBinderTests
                 return Task.CompletedTask;
             });
 
-        await AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, CancellationToken.None);
+        await AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, _noopUseHttps, CancellationToken.None);
 
         Assert.True(ipV4Attempt, "Should have attempted to bind to IPAddress.Any");
         Assert.True(ipV6Attempt, "Should have attempted to bind to IPAddress.IPv6Any");
@@ -315,7 +309,7 @@ public class AddressBinderTests
                 return Task.CompletedTask;
             });
 
-        await AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, CancellationToken.None);
+        await AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, _noopUseHttps, CancellationToken.None);
 
         Assert.Contains(endpoints, e => e.IPEndPoint.Port == 5000 && !e.IsTls);
     }

+ 1 - 0
src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs

@@ -84,6 +84,7 @@ public class KestrelServerOptionsTests
         serviceCollection.AddSingleton(Mock.Of<IHostEnvironment>());
         serviceCollection.AddSingleton(Mock.Of<ILogger<KestrelServer>>());
         serviceCollection.AddSingleton(Mock.Of<ILogger<HttpsConnectionMiddleware>>());
+        serviceCollection.AddSingleton(Mock.Of<IHttpsConfigurationService>());
         options.ApplicationServices = serviceCollection.BuildServiceProvider();
 
         options.Configure();

+ 19 - 1
src/Servers/Kestrel/Core/test/KestrelServerTests.cs

@@ -28,9 +28,15 @@ public class KestrelServerTests
 {
     private KestrelServerOptions CreateServerOptions()
     {
+        // It's not actually going to be used - we just need to satisfy the check in ApplyDefaultCertificate
+        var mockHttpsConfig = new Mock<IHttpsConfigurationService>();
+        mockHttpsConfig.Setup(m => m.IsInitialized).Returns(true);
+
         var serverOptions = new KestrelServerOptions();
         serverOptions.ApplicationServices = new ServiceCollection()
             .AddSingleton(new KestrelMetrics(new TestMeterFactory()))
+            .AddSingleton(Mock.Of<IHostEnvironment>())
+            .AddSingleton(mockHttpsConfig.Object)
             .AddLogging()
             .BuildServiceProvider();
         return serverOptions;
@@ -287,10 +293,20 @@ public class KestrelServerTests
         ILoggerFactory loggerFactory = null,
         KestrelMetrics metrics = null)
     {
+        var httpsConfigurationService = new HttpsConfigurationService();
+        if (options?.ApplicationServices is IServiceProvider serviceProvider)
+        {
+            httpsConfigurationService.Initialize(
+                serviceProvider.GetRequiredService<IHostEnvironment>(),
+                serviceProvider.GetRequiredService<ILogger<KestrelServer>>(),
+                serviceProvider.GetRequiredService<ILogger<HttpsConnectionMiddleware>>());
+        }
+	
         return new KestrelServerImpl(
             Options.Create<KestrelServerOptions>(options),
             transportFactories,
             multiplexedFactories,
+            httpsConfigurationService,
             loggerFactory ?? new LoggerFactory(new[] { new KestrelTestLoggerProvider() }),
             metrics ?? new KestrelMetrics(new TestMeterFactory()));
     }
@@ -714,7 +730,7 @@ public class KestrelServerTests
             testContext.Log,
             Heartbeat.Interval);
 
-        using (var server = new KestrelServerImpl(new[] { new MockTransportFactory() }, Array.Empty<IMultiplexedConnectionListenerFactory>(), testContext))
+        using (var server = new KestrelServerImpl(new[] { new MockTransportFactory() }, Array.Empty<IMultiplexedConnectionListenerFactory>(), new HttpsConfigurationService(), testContext))
         {
             Assert.Null(testContext.DateHeaderValueManager.GetDateHeaderValues());
 
@@ -768,6 +784,7 @@ public class KestrelServerTests
         serviceCollection.AddSingleton(Mock.Of<IHostEnvironment>());
         serviceCollection.AddSingleton(Mock.Of<ILogger<KestrelServer>>());
         serviceCollection.AddSingleton(Mock.Of<ILogger<HttpsConnectionMiddleware>>());
+        serviceCollection.AddSingleton(Mock.Of<IHttpsConfigurationService>());
 
         var options = new KestrelServerOptions
         {
@@ -905,6 +922,7 @@ public class KestrelServerTests
         serviceCollection.AddSingleton(Mock.Of<IHostEnvironment>());
         serviceCollection.AddSingleton(Mock.Of<ILogger<KestrelServer>>());
         serviceCollection.AddSingleton(Mock.Of<ILogger<HttpsConnectionMiddleware>>());
+        serviceCollection.AddSingleton(Mock.Of<IHttpsConfigurationService>());
 
         var options = new KestrelServerOptions
         {

+ 2 - 0
src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt

@@ -1 +1,3 @@
 #nullable enable
+static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelHttpsConfiguration(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
+static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelCore(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!

+ 50 - 8
src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs

@@ -19,6 +19,28 @@ namespace Microsoft.AspNetCore.Hosting;
 /// </summary>
 public static class WebHostBuilderKestrelExtensions
 {
+    /// <summary>
+    /// In <see cref="UseKestrelCore(IWebHostBuilder)"/> scenarios, it may be necessary to explicitly
+    /// opt in to certain HTTPS functionality.  For example, if <code>ASPNETCORE_URLS</code> includes
+    /// an <code>https://</code> address, <see cref="UseKestrelHttpsConfiguration"/> will enable configuration
+    /// of HTTPS on that endpoint.
+    ///
+    /// Has no effect in <see cref="UseKestrel(IWebHostBuilder)"/> scenarios.
+    /// </summary>
+    /// <param name="hostBuilder">
+    /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder to configure.
+    /// </param>
+    /// <returns>
+    /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder.
+    /// </returns>
+    public static IWebHostBuilder UseKestrelHttpsConfiguration(this IWebHostBuilder hostBuilder)
+    {
+        return hostBuilder.ConfigureServices(services =>
+        {
+            services.AddSingleton<HttpsConfigurationService.IInitializer, HttpsConfigurationService.Initializer>();
+        });
+    }
+
     /// <summary>
     /// Specify Kestrel as the server to be used by the web host.
     /// </summary>
@@ -29,6 +51,33 @@ public static class WebHostBuilderKestrelExtensions
     /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder.
     /// </returns>
     public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder)
+    {
+        return hostBuilder
+            .UseKestrelCore()
+            .UseKestrelHttpsConfiguration()
+            .UseQuic(options =>
+            {
+                // Configure server defaults to match client defaults.
+                // https://github.com/dotnet/runtime/blob/a5f3676cc71e176084f0f7f1f6beeecd86fbeafc/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs#L118-L119
+                options.DefaultStreamErrorCode = (long)Http3ErrorCode.RequestCancelled;
+                options.DefaultCloseErrorCode = (long)Http3ErrorCode.NoError;
+            });
+    }
+
+    /// <summary>
+    /// Specify Kestrel as the server to be used by the web host.
+    /// Includes less automatic functionality than <see cref="UseKestrel(IWebHostBuilder)"/> to make trimming more effective
+    /// (e.g. for <see href="https://aka.ms/aspnet/nativeaot">Native AOT</see> scenarios).  If the host ends up depending on
+    /// some of the absent functionality, a best-effort attempt will be made to enable it on-demand.  Failing that, an
+    /// exception with an informative error message will be raised when the host is started.
+    /// </summary>
+    /// <param name="hostBuilder">
+    /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder to configure.
+    /// </param>
+    /// <returns>
+    /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder.
+    /// </returns>
+    public static IWebHostBuilder UseKestrelCore(this IWebHostBuilder hostBuilder)
     {
         hostBuilder.ConfigureServices(services =>
         {
@@ -36,18 +85,11 @@ public static class WebHostBuilderKestrelExtensions
             services.TryAddSingleton<IConnectionListenerFactory, SocketTransportFactory>();
 
             services.AddTransient<IConfigureOptions<KestrelServerOptions>, KestrelServerOptionsSetup>();
+            services.AddSingleton<IHttpsConfigurationService, HttpsConfigurationService>();
             services.AddSingleton<IServer, KestrelServerImpl>();
             services.AddSingleton<KestrelMetrics>();
         });
 
-        hostBuilder.UseQuic(options =>
-        {
-            // Configure server defaults to match client defaults.
-            // https://github.com/dotnet/runtime/blob/a5f3676cc71e176084f0f7f1f6beeecd86fbeafc/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs#L118-L119
-            options.DefaultStreamErrorCode = (long)Http3ErrorCode.RequestCancelled;
-            options.DefaultCloseErrorCode = (long)Http3ErrorCode.NoError;
-        });
-
         if (OperatingSystem.IsWindows())
         {
             hostBuilder.UseNamedPipes();

+ 237 - 0
src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs

@@ -0,0 +1,237 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Microsoft.AspNetCore.Server.Kestrel.Https;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Tests;
+
+public class HttpsConfigurationTests
+{
+    [Theory]
+    [InlineData("http://127.0.0.1:0", true)]
+    [InlineData("http://127.0.0.1:0", false)]
+    [InlineData("https://127.0.0.1:0", true)]
+    [InlineData("https://127.0.0.1:0", false)]
+    public async Task BindAddressFromSetting(string address, bool useKestrelHttpsConfiguration)
+    {
+        var hostBuilder = new WebHostBuilder()
+                .UseKestrelCore()
+                .ConfigureKestrel(serverOptions =>
+                {
+                    serverOptions.TestOverrideDefaultCertificate = new X509Certificate2(Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx"), "testPassword");
+                })
+                .Configure(app => { });
+
+        // This is what ASPNETCORE_URLS would populate
+        hostBuilder.UseSetting(WebHostDefaults.ServerUrlsKey, address);
+
+        if (useKestrelHttpsConfiguration)
+        {
+            hostBuilder.UseKestrelHttpsConfiguration();
+        }
+
+        var host = hostBuilder.Build();
+
+        Assert.Single(host.ServerFeatures.Get<IServerAddressesFeature>().Addresses, address);
+
+        if (address.StartsWith("https", StringComparison.OrdinalIgnoreCase) && !useKestrelHttpsConfiguration)
+        {
+            Assert.Throws<InvalidOperationException>(host.Run);
+        }
+        else
+        {
+            // Binding succeeds
+            await host.StartAsync();
+            await host.StopAsync();
+        }
+    }
+
+    [Fact]
+    public void NoFallbackToHttpAddress()
+    {
+        const string httpAddress = "http://127.0.0.1:0";
+        const string httpsAddress = "https://localhost:5001";
+
+        var hostBuilder = new WebHostBuilder()
+                .UseKestrelCore()
+                .Configure(app => { });
+
+        // This is what ASPNETCORE_URLS would populate
+        hostBuilder.UseSetting(WebHostDefaults.ServerUrlsKey, $"{httpAddress};{httpsAddress}");
+
+        var host = hostBuilder.Build();
+
+        Assert.Equal(new[] { httpAddress, httpsAddress }, host.ServerFeatures.Get<IServerAddressesFeature>().Addresses);
+
+        Assert.Throws<InvalidOperationException>(host.Run);
+    }
+
+    [Theory]
+    [InlineData("http://127.0.0.1:0", true)]
+    [InlineData("http://127.0.0.1:0", false)]
+    [InlineData("https://127.0.0.1:0", true)]
+    [InlineData("https://127.0.0.1:0", false)]
+    public async Task BindAddressFromEndpoint(string address, bool useKestrelHttpsConfiguration)
+    {
+        var hostBuilder = new WebHostBuilder()
+                .UseKestrelCore()
+                .ConfigureKestrel(serverOptions =>
+                {
+                    var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
+                    {
+                        new KeyValuePair<string, string>("Endpoints:end1:Url", address),
+                        new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx")),
+                        new KeyValuePair<string, string>("Certificates:Default:Password", "testPassword"),
+                    }).Build();
+                    serverOptions.Configure(config);
+                })
+                .Configure(app => { });
+
+        if (useKestrelHttpsConfiguration)
+        {
+            hostBuilder.UseKestrelHttpsConfiguration();
+        }
+
+        var host = hostBuilder.Build();
+
+        if (address.StartsWith("https", StringComparison.OrdinalIgnoreCase) && !useKestrelHttpsConfiguration)
+        {
+            Assert.Throws<InvalidOperationException>(host.Run);
+        }
+        else
+        {
+            // Binding succeeds
+            await host.StartAsync();
+            await host.StopAsync();
+        }
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public async Task LoadDefaultCertificate(bool useKestrelHttpsConfiguration)
+    {
+        var hostBuilder = new WebHostBuilder()
+                .UseKestrelCore()
+                .ConfigureKestrel(serverOptions =>
+                {
+                    var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
+                    {
+                        new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx")),
+                        new KeyValuePair<string, string>("Certificates:Default:Password", "testPassword"),
+                    }).Build();
+                    serverOptions.Configure(config);
+                })
+                .Configure(app => { });
+
+        if (useKestrelHttpsConfiguration)
+        {
+            hostBuilder.UseKestrelHttpsConfiguration();
+        }
+
+        var host = hostBuilder.Build();
+
+        // There's no exception for specifying a default cert when https config is enabled
+        await host.StartAsync();
+        await host.StopAsync();
+    }
+
+    [Theory]
+    [InlineData("http://127.0.0.1:0", true)]
+    [InlineData("http://127.0.0.1:0", false)]
+    [InlineData("https://127.0.0.1:0", true)]
+    [InlineData("https://127.0.0.1:0", false)]
+    public async Task LoadEndpointCertificate(string address, bool useKestrelHttpsConfiguration)
+    {
+        var hostBuilder = new WebHostBuilder()
+                .UseKestrelCore()
+                .ConfigureKestrel(serverOptions =>
+                {
+                    var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
+                    {
+                        new KeyValuePair<string, string>("Endpoints:end1:Url", address),
+                        new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx")),
+                        new KeyValuePair<string, string>("Certificates:Default:Password", "testPassword"),
+                    }).Build();
+                    serverOptions.Configure(config);
+                })
+                .Configure(app => { });
+
+        if (useKestrelHttpsConfiguration)
+        {
+            hostBuilder.UseKestrelHttpsConfiguration();
+        }
+
+        var host = hostBuilder.Build();
+
+        if (address.StartsWith("https", StringComparison.OrdinalIgnoreCase) && !useKestrelHttpsConfiguration)
+        {
+            Assert.Throws<InvalidOperationException>(host.Run);
+        }
+        else
+        {
+            // Binding succeeds
+            await host.StartAsync();
+            await host.StopAsync();
+        }
+    }
+
+    [Fact]
+    public async Task UseHttpsJustWorks()
+    {
+        var hostBuilder = new WebHostBuilder()
+            .UseKestrelCore()
+            .ConfigureKestrel(serverOptions =>
+            {
+                serverOptions.TestOverrideDefaultCertificate = new X509Certificate2(Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx"), "testPassword");
+
+                serverOptions.ListenAnyIP(0, listenOptions =>
+                {
+                    listenOptions.UseHttps();
+                });
+            })
+            .Configure(app => { });
+
+        var host = hostBuilder.Build();
+
+        // Binding succeeds
+        await host.StartAsync();
+        await host.StopAsync();
+
+        Assert.True(host.Services.GetRequiredService<IHttpsConfigurationService>().IsInitialized);
+    }
+
+    [Fact]
+    public async Task UseHttpsMayNotImplyUseKestrelHttpsConfiguration()
+    {
+        var hostBuilder = new WebHostBuilder()
+            .UseKestrelCore()
+            .ConfigureKestrel(serverOptions =>
+            {
+                serverOptions.ListenAnyIP(0, listenOptions =>
+                {
+                    listenOptions.UseHttps(new HttpsConnectionAdapterOptions()
+                    {
+                        ServerCertificate = new X509Certificate2(Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx"), "testPassword"),
+                    });
+                });
+            })
+            .Configure(app => { });
+
+        var host = hostBuilder.Build();
+
+        // Binding succeeds
+        await host.StartAsync();
+        await host.StopAsync();
+
+        // This is more documentary than normative
+        Assert.False(host.Services.GetRequiredService<IHttpsConfigurationService>().IsInitialized);
+    }
+}

+ 2 - 0
src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs

@@ -27,6 +27,8 @@ public class KestrelConfigurationLoaderTests
             .AddLogging()
             .AddSingleton<IHostEnvironment>(env)
             .AddSingleton(new KestrelMetrics(new TestMeterFactory()))
+            .AddSingleton<IHttpsConfigurationService, HttpsConfigurationService>()
+            .AddSingleton<HttpsConfigurationService.IInitializer, HttpsConfigurationService.Initializer>()
             .BuildServiceProvider();
         return serverOptions;
     }

+ 3 - 1
src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs

@@ -84,6 +84,8 @@ internal class TestServer : IAsyncDisposable, IStartup
                     {
                         services.AddSingleton<IStartup>(this);
                         services.AddSingleton(context.LoggerFactory);
+                        services.AddSingleton<IHttpsConfigurationService, HttpsConfigurationService>();
+                        services.AddSingleton<HttpsConfigurationService.IInitializer, HttpsConfigurationService.Initializer>();
                         services.AddSingleton<IServer>(sp =>
                         {
                             // Manually configure options on the TestServiceContext.
@@ -94,7 +96,7 @@ internal class TestServer : IAsyncDisposable, IStartup
                                 c.Configure(context.ServerOptions);
                             }
 
-                            return new KestrelServerImpl(sp.GetServices<IConnectionListenerFactory>(), Array.Empty<IMultiplexedConnectionListenerFactory>(), context);
+                            return new KestrelServerImpl(sp.GetServices<IConnectionListenerFactory>(), Array.Empty<IMultiplexedConnectionListenerFactory>(), sp.GetRequiredService<IHttpsConfigurationService>(), context);
                         });
                         configureServices(services);
                     })

+ 7 - 6
src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs

@@ -35,9 +35,15 @@ public class HttpsConnectionMiddlewareTests : LoggedTest
 
     private static KestrelServerOptions CreateServerOptions()
     {
+        var env = new Mock<IHostEnvironment>();
+        env.SetupGet(e => e.ContentRootPath).Returns(Directory.GetCurrentDirectory());
+
         var serverOptions = new KestrelServerOptions();
         serverOptions.ApplicationServices = new ServiceCollection()
             .AddLogging()
+            .AddSingleton<IHttpsConfigurationService, HttpsConfigurationService>()
+            .AddSingleton<HttpsConfigurationService.IInitializer, HttpsConfigurationService.Initializer>()
+            .AddSingleton(env.Object)
             .AddSingleton(new KestrelMetrics(new TestMeterFactory()))
             .BuildServiceProvider();
         return serverOptions;
@@ -73,14 +79,9 @@ public class HttpsConnectionMiddlewareTests : LoggedTest
             ["Certificates:Default:Password"] = "aspnetcore",
         }).Build();
 
-        var env = new Mock<IHostEnvironment>();
-        env.SetupGet(e => e.ContentRootPath).Returns(Directory.GetCurrentDirectory());
-
         var options = CreateServerOptions();
 
-        var logger = options.ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
-        var httpsLogger = options.ApplicationServices.GetRequiredService<ILogger<HttpsConnectionMiddleware>>();
-        var loader = new KestrelConfigurationLoader(options, configuration, env.Object, reloadOnChange: false, logger, httpsLogger);
+        var loader = new KestrelConfigurationLoader(options, configuration, options.ApplicationServices.GetRequiredService<IHttpsConfigurationService>(), reloadOnChange: false);
         options.ConfigurationLoader = loader; // Since we're constructing it explicitly, we have to hook it up explicitly
         loader.Load();
 

+ 8 - 0
src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs

@@ -21,10 +21,12 @@ using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
 using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport;
 using Microsoft.AspNetCore.Testing;
 using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Internal;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Metrics;
 using Microsoft.Extensions.Options;
+using Moq;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests;
@@ -35,9 +37,15 @@ public class HttpsTests : LoggedTest
 
     private static KestrelServerOptions CreateServerOptions()
     {
+        // It's not actually going to be used - we just need to satisfy the check in ApplyDefaultCertificate
+        var mockHttpsConfig = new Mock<IHttpsConfigurationService>();
+        mockHttpsConfig.Setup(m => m.IsInitialized).Returns(true);
+
         var serverOptions = new KestrelServerOptions();
         serverOptions.ApplicationServices = new ServiceCollection()
             .AddLogging()
+            .AddSingleton(mockHttpsConfig.Object)
+            .AddSingleton(Mock.Of<IHostEnvironment>())
             .AddSingleton(new KestrelMetrics(new TestMeterFactory()))
             .BuildServiceProvider();
         return serverOptions;

+ 3 - 0
src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs

@@ -87,6 +87,8 @@ internal class TestServer : IAsyncDisposable, IStartup
                 services.AddSingleton<IStartup>(this);
                 services.AddSingleton(context.LoggerFactory);
                 services.AddSingleton(context.Metrics);
+                services.AddSingleton<IHttpsConfigurationService, HttpsConfigurationService>();
+                services.AddSingleton<HttpsConfigurationService.IInitializer, HttpsConfigurationService.Initializer>();
 
                 services.AddSingleton<IServer>(sp =>
                 {
@@ -95,6 +97,7 @@ internal class TestServer : IAsyncDisposable, IStartup
                     return new KestrelServerImpl(
                         new IConnectionListenerFactory[] { _transportFactory },
                         sp.GetServices<IMultiplexedConnectionListenerFactory>(),
+                        sp.GetRequiredService<IHttpsConfigurationService>(),
                         context);
                 });
             });

+ 93 - 0
src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs

@@ -10,6 +10,8 @@ using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Server.Kestrel.Core;
 using Microsoft.AspNetCore.Server.Kestrel.Https;
 using Microsoft.AspNetCore.Testing;
+using Microsoft.CSharp.RuntimeBinder;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 using Xunit;
@@ -329,6 +331,97 @@ public class Http3TlsTests : LoggedTest
         await host.StopAsync().DefaultTimeout();
     }
 
+    [ConditionalTheory]
+    [MsQuicSupported]
+    [InlineData(true, true, true)]
+    [InlineData(true, true, false)]
+    [InlineData(true, false, false)]
+    [InlineData(false, true, true)]
+    [InlineData(false, true, false)]
+    [InlineData(false, false, false)]
+    public async Task UseKestrelCore_CodeBased(bool useQuic, bool useHttps, bool useHttpsEnablesHttpsConfiguration)
+    {
+        var hostBuilder = new WebHostBuilder()
+                .UseKestrelCore()
+                .ConfigureKestrel(serverOptions =>
+                {
+                    serverOptions.ListenAnyIP(0, listenOptions =>
+                    {
+                        listenOptions.Protocols = HttpProtocols.Http3;
+                        if (useHttps)
+                        {
+                            if (useHttpsEnablesHttpsConfiguration)
+                            {
+                                listenOptions.UseHttps(httpsOptions =>
+                                {
+                                    httpsOptions.ServerCertificate = TestResources.GetTestCertificate();
+                                });
+                            }
+                            else
+                            {
+                                // Specifically choose an overload that doesn't enable https configuration
+                                listenOptions.UseHttps(new HttpsConnectionAdapterOptions
+                                {
+                                    ServerCertificate = TestResources.GetTestCertificate()
+                                });
+                            }
+                        }
+                    });
+                })
+                .Configure(app => { });
+
+        if (useQuic)
+        {
+            hostBuilder.UseQuic();
+        }
+
+        var host = hostBuilder.Build();
+
+        if (useHttps && useHttpsEnablesHttpsConfiguration && useQuic)
+        {
+            // Binding succeeds
+            await host.StartAsync();
+            await host.StopAsync();
+        }
+        else
+        {
+            // This *could* work for `useHttps && !useHttpsEnablesHttpsConfiguration` if `UseQuic` implied `UseKestrelHttpsConfiguration`
+            Assert.Throws<InvalidOperationException>(host.Run);
+        }
+    }
+
+    [ConditionalTheory]
+    [MsQuicSupported]
+    [InlineData(true)]
+    [InlineData(false)]
+    public void UseKestrelCore_ConfigurationBased(bool useQuic)
+    {
+        var hostBuilder = new WebHostBuilder()
+                .UseKestrelCore()
+                .ConfigureKestrel(serverOptions =>
+                {
+                    var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
+                    {
+                        new KeyValuePair<string, string>("Endpoints:end1:Url", "https://127.0.0.1:0"),
+                        new KeyValuePair<string, string>("Endpoints:end1:Protocols", "Http3"),
+                        new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx")),
+                        new KeyValuePair<string, string>("Certificates:Default:Password", "testPassword"),
+                    }).Build();
+                    serverOptions.Configure(config);
+                })
+                .Configure(app => { });
+
+        if (useQuic)
+        {
+            hostBuilder.UseQuic();
+        }
+
+        var host = hostBuilder.Build();
+
+        // This *could* work (in some cases) if `UseQuic` implied `UseKestrelHttpsConfiguration`
+        Assert.Throws<InvalidOperationException>(host.Run);
+    }
+
     private IHostBuilder CreateHostBuilder(RequestDelegate requestDelegate, HttpProtocols? protocol = null, Action<KestrelServerOptions> configureKestrel = null)
     {
         return HttpHelpers.CreateHostBuilder(AddTestLogging, requestDelegate, protocol, configureKestrel);