diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index 2c7c3e8ef18d..bbec0353a368 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -734,4 +734,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l Call UseKestrelHttpsConfiguration() on IWebHostBuilder to automatically enable HTTPS when an https:// address is used. + + Call KestrelConfigurationLoader.Load() before retrieving the server certificate. + diff --git a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs index fdc16bd7cc45..1e40ff67bd16 100644 --- a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs +++ b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs @@ -18,7 +18,7 @@ public class KestrelConfigurationLoader { private readonly IHttpsConfigurationService _httpsConfigurationService; - private bool _loaded; + internal bool IsLoaded { get; private set; } internal KestrelConfigurationLoader( KestrelServerOptions options, @@ -234,12 +234,12 @@ internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions) /// public void Load() { - if (_loaded) + if (IsLoaded) { // The loader has already been run. return; } - _loaded = true; + IsLoaded = true; Reload(); diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index b44e3b33adcf..9e76ddf41787 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -585,4 +585,27 @@ public void ListenNamedPipe(string pipeName, Action configure) configure(listenOptions); CodeBackedListenOptions.Add(listenOptions); } + + /// + /// Return true if there is a default or development server certificate. + /// Should not be called before the configuration is loaded, if there is one. + /// + /// True if there is a default or development server certificate, false otherwise. + /// If there is a configuration and it has not been loaded. + public bool CheckDefaultCertificate() + { + if (ConfigurationLoader is { IsLoaded: false }) + { + throw new InvalidOperationException(CoreStrings.NeedConfigurationToRetrieveServerCertificate); + } + + // We consider calls to `CheckDefaultCertificate` to be a clear expression of user intent to pull in HTTPS configuration support + EnableHttpsConfiguration(); + + var httpsOptions = new HttpsConnectionAdapterOptions(); + ApplyHttpsDefaults(httpsOptions); + ApplyDefaultCertificate(httpsOptions); + + return httpsOptions.ServerCertificate is not null; + } } diff --git a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt index 5a2c7ffb53b1..39e50372a7e5 100644 --- a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt @@ -1,6 +1,7 @@ #nullable enable Microsoft.AspNetCore.Server.Kestrel.Core.Features.ISslStreamFeature Microsoft.AspNetCore.Server.Kestrel.Core.Features.ISslStreamFeature.SslStream.get -> System.Net.Security.SslStream! +Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.CheckDefaultCertificate() -> bool Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ListenNamedPipe(string! pipeName) -> void Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ListenNamedPipe(string! pipeName, System.Action! configure) -> void -Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions.PipeName.get -> string? \ No newline at end of file +Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions.PipeName.get -> string? diff --git a/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs b/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs index cb27ce5bc210..546ec9c8d0ca 100644 --- a/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs @@ -183,6 +183,29 @@ public async Task LoadEndpointCertificate(string address, bool useKestrelHttpsCo } } + [Fact] + public async Task CheckDefaultCertificateJustWorks() + { + var hostBuilder = new WebHostBuilder() + .UseKestrelCore() + .ConfigureKestrel(serverOptions => + { + var testCertificate = new X509Certificate2(Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx"), "testPassword"); + serverOptions.TestOverrideDefaultCertificate = testCertificate; + + Assert.True(serverOptions.CheckDefaultCertificate()); + }) + .Configure(app => { }); + + var host = hostBuilder.Build(); + + // Binding succeeds + await host.StartAsync(); + await host.StopAsync(); + + Assert.True(host.Services.GetRequiredService().IsInitialized); + } + [Fact] public async Task UseHttpsJustWorks() { diff --git a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs index 5b033b4952ef..dbf867221e44 100644 --- a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs @@ -384,6 +384,56 @@ void CheckListenOptions(X509Certificate2 expectedCert) } } + [Fact] + public void CheckDefaultCertificate_DevelopmentCertificate() + { + try + { + var serverOptions = CreateServerOptions(); + var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable); + var bytes = certificate.Export(X509ContentType.Pkcs12, "1234"); + var path = GetCertificatePath(); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + File.WriteAllBytes(path, bytes); + + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Certificates:Development:Password", "1234"), + }).Build(); + + serverOptions.Configure(config); + serverOptions.ConfigurationLoader.Load(); + + Assert.True(serverOptions.CheckDefaultCertificate()); + } + finally + { + if (File.Exists(GetCertificatePath())) + { + File.Delete(GetCertificatePath()); + } + } + } + + [Fact] + public void CheckDefaultCertificate_DefaultCertificate() + { + var certificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword", X509KeyStorageFlags.Exportable); + + var serverOptions = CreateServerOptions(); + + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Certificates:Default:Path", TestResources.TestCertificatePath), + new KeyValuePair("Certificates:Default:Password", "testPassword") + }).Build(); + + serverOptions.Configure(config); + serverOptions.ConfigurationLoader.Load(); + + Assert.True(serverOptions.CheckDefaultCertificate()); + } + [Fact] public void ConfigureEndpoint_ThrowsWhen_The_PasswordIsMissing() { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs index 819def76e5a2..a1026c7447b2 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs @@ -775,6 +775,59 @@ await sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null, Assert.True(onAuthenticateCalled, "onAuthenticateCalled"); } + [Fact] + public void CheckDefaultCertificate_ConfigurationNotLoaded() + { + var serverOptions = CreateServerOptions(); + serverOptions.TestOverrideDefaultCertificate = _x509Certificate2; + + serverOptions.Configure(); + + Assert.Throws(() => serverOptions.CheckDefaultCertificate()); + } + + [Fact] + public void CheckDefaultCertificate_ConfigurationLoaded() + { + var serverOptions = CreateServerOptions(); + serverOptions.TestOverrideDefaultCertificate = _x509Certificate2; + + serverOptions.Configure(); + serverOptions.ConfigurationLoader.Load(); + + Assert.True(serverOptions.CheckDefaultCertificate()); + } + + [Fact] + public void CheckDefaultCertificate_NoConfiguration() + { + var serverOptions = CreateServerOptions(); + serverOptions.TestOverrideDefaultCertificate = _x509Certificate2; + + Assert.True(serverOptions.CheckDefaultCertificate()); + } + + [Fact] + public void CheckDefaultCertificate_NoCertificate() + { + var serverOptions = CreateServerOptions(); + serverOptions.IsDevelopmentCertificateLoaded = true; // Prevent the system default from being loaded + + Assert.False(serverOptions.CheckDefaultCertificate()); + } + + [Fact] + public void CheckDefaultCertificate_FromHttpsDefaults() + { + var serverOptions = CreateServerOptions(); + serverOptions.ConfigureHttpsDefaults(httpsOptions => + { + httpsOptions.ServerCertificate = _x509Certificate2; + }); + + Assert.True(serverOptions.CheckDefaultCertificate()); + } + private class HandshakeErrorLoggerProvider : ILoggerProvider { public HttpsConnectionFilterLogger FilterLogger { get; set; } = new HttpsConnectionFilterLogger();