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();