diff --git a/src/Kestrel.Core/Internal/KestrelServerOptionsSetup.cs b/src/Kestrel.Core/Internal/KestrelServerOptionsSetup.cs index 013ce0ac80..18c96e2039 100644 --- a/src/Kestrel.Core/Internal/KestrelServerOptionsSetup.cs +++ b/src/Kestrel.Core/Internal/KestrelServerOptionsSetup.cs @@ -2,12 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Linq; -using System.Security.Cryptography.X509Certificates; -using Microsoft.AspNetCore.Certificates.Generation; -using Microsoft.AspNetCore.Server.Kestrel.Internal; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal @@ -24,33 +18,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal public void Configure(KestrelServerOptions options) { options.ApplicationServices = _services; - UseDefaultDeveloperCertificate(options); - } - - private void UseDefaultDeveloperCertificate(KestrelServerOptions options) - { - var logger = options.ApplicationServices.GetRequiredService>(); - X509Certificate2 certificate = null; - try - { - var certificateManager = new CertificateManager(); - certificate = certificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true) - .FirstOrDefault(); - } - catch - { - logger.UnableToLocateDevelopmentCertificate(); - } - - if (certificate != null) - { - logger.LocatedDevelopmentCertificate(certificate); - options.DefaultCertificate = certificate; - } - else - { - logger.UnableToLocateDevelopmentCertificate(); - } } } } diff --git a/src/Kestrel.Core/KestrelConfigurationLoader.cs b/src/Kestrel.Core/KestrelConfigurationLoader.cs index b6468374b2..60204443f6 100644 --- a/src/Kestrel.Core/KestrelConfigurationLoader.cs +++ b/src/Kestrel.Core/KestrelConfigurationLoader.cs @@ -225,6 +225,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel // Specified httpsOptions.ServerCertificate = LoadCertificate(endpoint.Certificate, endpoint.Name) ?? httpsOptions.ServerCertificate; + + // Fallback + Options.ApplyDefaultCert(httpsOptions); } if (EndpointConfigurations.TryGetValue(endpoint.Name, out var configureEndpoint)) diff --git a/src/Kestrel.Core/KestrelServerOptions.cs b/src/Kestrel.Core/KestrelServerOptions.cs index b4354d4c0a..6c62edc336 100644 --- a/src/Kestrel.Core/KestrelServerOptions.cs +++ b/src/Kestrel.Core/KestrelServerOptions.cs @@ -3,12 +3,17 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Certificates.Generation; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Core { @@ -75,10 +80,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core private Action HttpsDefaults { get; set; } = _ => { }; /// - /// The default server certificate for https endpoints. This is applied before HttpsDefaults. + /// The default server certificate for https endpoints. This is applied lazily after HttpsDefaults and user options. /// internal X509Certificate2 DefaultCertificate { get; set; } + /// + /// Has the default dev certificate load been attempted? + /// + internal bool IsDevCertLoaded { get; set; } + /// /// Specifies a configuration Action to run for each newly created endpoint. Calling this again will replace /// the prior action. @@ -105,10 +115,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions) { - httpsOptions.ServerCertificate = DefaultCertificate; HttpsDefaults(httpsOptions); } + internal void ApplyDefaultCert(HttpsConnectionAdapterOptions httpsOptions) + { + if (httpsOptions.ServerCertificate != null || httpsOptions.ServerCertificateSelector != null) + { + return; + } + + EnsureDefaultCert(); + + httpsOptions.ServerCertificate = DefaultCertificate; + } + + private void EnsureDefaultCert() + { + if (DefaultCertificate == null && !IsDevCertLoaded) + { + IsDevCertLoaded = true; // Only try once + var logger = ApplicationServices.GetRequiredService>(); + try + { + var certificateManager = new CertificateManager(); + DefaultCertificate = certificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true) + .FirstOrDefault(); + + if (DefaultCertificate != null) + { + logger.LocatedDevelopmentCertificate(DefaultCertificate); + } + else + { + logger.UnableToLocateDevelopmentCertificate(); + } + } + catch + { + logger.UnableToLocateDevelopmentCertificate(); + } + } + } + /// /// Creates a configuration loader for setting up Kestrel. /// diff --git a/src/Kestrel.Core/ListenOptionsHttpsExtensions.cs b/src/Kestrel.Core/ListenOptionsHttpsExtensions.cs index 162ee59d40..80c6eda3d2 100644 --- a/src/Kestrel.Core/ListenOptionsHttpsExtensions.cs +++ b/src/Kestrel.Core/ListenOptionsHttpsExtensions.cs @@ -178,6 +178,7 @@ namespace Microsoft.AspNetCore.Hosting var options = new HttpsConnectionAdapterOptions(); listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options); configureOptions(options); + listenOptions.KestrelServerOptions.ApplyDefaultCert(options); if (options.ServerCertificate == null && options.ServerCertificateSelector == null) { @@ -191,6 +192,7 @@ namespace Microsoft.AspNetCore.Hosting { var options = new HttpsConnectionAdapterOptions(); listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options); + listenOptions.KestrelServerOptions.ApplyDefaultCert(options); if (options.ServerCertificate == null && options.ServerCertificateSelector == null) { diff --git a/test/Kestrel.Core.Tests/KestrelServerTests.cs b/test/Kestrel.Core.Tests/KestrelServerTests.cs index 10de4f2fa6..e8a2ca2a13 100644 --- a/test/Kestrel.Core.Tests/KestrelServerTests.cs +++ b/test/Kestrel.Core.Tests/KestrelServerTests.cs @@ -65,7 +65,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void KestrelServerThrowsUsefulExceptionIfDefaultHttpsProviderNotAdded() { - using (var server = CreateServer(CreateServerOptions(), throwOnCriticalErrors: false)) + var options = CreateServerOptions(); + options.IsDevCertLoaded = true; // Prevent the system default from being loaded + using (var server = CreateServer(options, throwOnCriticalErrors: false)) { server.Features.Get().Addresses.Add("https://127.0.0.1:0"); diff --git a/test/Kestrel.FunctionalTests/HttpsTests.cs b/test/Kestrel.FunctionalTests/HttpsTests.cs index 481f3ef2bd..29a598dee1 100644 --- a/test/Kestrel.FunctionalTests/HttpsTests.cs +++ b/test/Kestrel.FunctionalTests/HttpsTests.cs @@ -47,25 +47,56 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests options.UseHttps(); }); + Assert.False(serverOptions.IsDevCertLoaded); + serverOptions.ListenLocalhost(5001, options => { options.UseHttps(opt => { - Assert.Equal(defaultCert, opt.ServerCertificate); + // The default cert is applied after UseHttps. + Assert.Null(opt.ServerCertificate); }); }); + Assert.False(serverOptions.IsDevCertLoaded); } [Fact] - public void ConfigureHttpsDefaultsOverridesDefaultCert() + public void ConfigureHttpsDefaultsNeverLoadsDefaultCert() { var serverOptions = CreateServerOptions(); - var defaultCert = new X509Certificate2(TestResources.TestCertificatePath, "testPassword"); - serverOptions.DefaultCertificate = defaultCert; + var testCert = new X509Certificate2(TestResources.TestCertificatePath, "testPassword"); serverOptions.ConfigureHttpsDefaults(options => { - Assert.Equal(defaultCert, options.ServerCertificate); - options.ServerCertificate = null; + Assert.Null(options.ServerCertificate); + options.ServerCertificate = testCert; + options.ClientCertificateMode = ClientCertificateMode.RequireCertificate; + }); + serverOptions.ListenLocalhost(5000, options => + { + options.UseHttps(opt => + { + Assert.Equal(testCert, opt.ServerCertificate); + Assert.Equal(ClientCertificateMode.RequireCertificate, opt.ClientCertificateMode); + }); + }); + // Never lazy loaded + Assert.False(serverOptions.IsDevCertLoaded); + Assert.Null(serverOptions.DefaultCertificate); + } + + [Fact] + public void ConfigureCertSelectorNeverLoadsDefaultCert() + { + var serverOptions = CreateServerOptions(); + var testCert = new X509Certificate2(TestResources.TestCertificatePath, "testPassword"); + serverOptions.ConfigureHttpsDefaults(options => + { + Assert.Null(options.ServerCertificate); + Assert.Null(options.ServerCertificateSelector); + options.ServerCertificateSelector = (features, name) => + { + return testCert; + }; options.ClientCertificateMode = ClientCertificateMode.RequireCertificate; }); serverOptions.ListenLocalhost(5000, options => @@ -73,12 +104,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests options.UseHttps(opt => { Assert.Null(opt.ServerCertificate); + Assert.NotNull(opt.ServerCertificateSelector); Assert.Equal(ClientCertificateMode.RequireCertificate, opt.ClientCertificateMode); - - // So UseHttps won't throw - opt.ServerCertificate = defaultCert; }); }); + // Never lazy loaded + Assert.False(serverOptions.IsDevCertLoaded); + Assert.Null(serverOptions.DefaultCertificate); } [Fact]