Skip to content

Commit 9eff630

Browse files
committed
Introduce KestrelServerOptions.HasDefaultOrDevelopmentCertificate
...as a way to determine whether a certificate is available before invoking `KestrelServerOptions.Listen` and possibly failing in `ListenOptions.UseHttps`. Fixes dotnet#28120
1 parent 7f4ee4a commit 9eff630

File tree

7 files changed

+160
-4
lines changed

7 files changed

+160
-4
lines changed

src/Servers/Kestrel/Core/src/CoreStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,4 +734,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
734734
<data name="NeedHttpsConfigurationToBindHttpsAddresses" xml:space="preserve">
735735
<value>Call UseKestrelHttpsConfiguration() on IWebHostBuilder to automatically enable HTTPS when an https:// address is used.</value>
736736
</data>
737+
<data name="NeedConfigurationToRetrieveServerCertificate" xml:space="preserve">
738+
<value>Call KestrelConfigurationLoader.Load() before retrieving the server certificate.</value>
739+
</data>
737740
</root>

src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class KestrelConfigurationLoader
1818
{
1919
private readonly IHttpsConfigurationService _httpsConfigurationService;
2020

21-
private bool _loaded;
21+
internal bool IsLoaded { get; private set; }
2222

2323
internal KestrelConfigurationLoader(
2424
KestrelServerOptions options,
@@ -234,12 +234,12 @@ internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions)
234234
/// </summary>
235235
public void Load()
236236
{
237-
if (_loaded)
237+
if (IsLoaded)
238238
{
239239
// The loader has already been run.
240240
return;
241241
}
242-
_loaded = true;
242+
IsLoaded = true;
243243

244244
Reload();
245245

src/Servers/Kestrel/Core/src/KestrelServerOptions.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,4 +585,30 @@ public void ListenNamedPipe(string pipeName, Action<ListenOptions> configure)
585585
configure(listenOptions);
586586
CodeBackedListenOptions.Add(listenOptions);
587587
}
588+
589+
/// <summary>
590+
/// Return true if there is a default or development server certificate.
591+
/// Should not be called before the configuration is loaded, if there is one.
592+
/// </summary>
593+
/// <returns>True if there is a default or development server certificate, false otherwise.</returns>
594+
/// <exception cref="InvalidOperationException">If there is a configuration and it has not been loaded.</exception>
595+
public bool HasDefaultOrDevelopmentCertificate
596+
{
597+
get
598+
{
599+
if (ConfigurationLoader is { IsLoaded: false })
600+
{
601+
throw new InvalidOperationException(CoreStrings.NeedConfigurationToRetrieveServerCertificate);
602+
}
603+
604+
// We consider calls to `GetServerCertificate` to be a clear expression of user intent to pull in HTTPS configuration support
605+
EnableHttpsConfiguration();
606+
607+
var httpsOptions = new HttpsConnectionAdapterOptions();
608+
ApplyHttpsDefaults(httpsOptions);
609+
ApplyDefaultCertificate(httpsOptions);
610+
611+
return httpsOptions.ServerCertificate is not null;
612+
}
613+
}
588614
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#nullable enable
22
Microsoft.AspNetCore.Server.Kestrel.Core.Features.ISslStreamFeature
33
Microsoft.AspNetCore.Server.Kestrel.Core.Features.ISslStreamFeature.SslStream.get -> System.Net.Security.SslStream!
4+
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.HasDefaultOrDevelopmentCertificate.get -> bool
45
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ListenNamedPipe(string! pipeName) -> void
56
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ListenNamedPipe(string! pipeName, System.Action<Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions!>! configure) -> void
6-
Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions.PipeName.get -> string?
7+
Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions.PipeName.get -> string?

src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,29 @@ public async Task LoadEndpointCertificate(string address, bool useKestrelHttpsCo
183183
}
184184
}
185185

186+
[Fact]
187+
public async Task HasDefaultOrDevelopmentCertificateJustWorks()
188+
{
189+
var hostBuilder = new WebHostBuilder()
190+
.UseKestrelCore()
191+
.ConfigureKestrel(serverOptions =>
192+
{
193+
var testCertificate = new X509Certificate2(Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx"), "testPassword");
194+
serverOptions.TestOverrideDefaultCertificate = testCertificate;
195+
196+
Assert.True(serverOptions.HasDefaultOrDevelopmentCertificate);
197+
})
198+
.Configure(app => { });
199+
200+
var host = hostBuilder.Build();
201+
202+
// Binding succeeds
203+
await host.StartAsync();
204+
await host.StopAsync();
205+
206+
Assert.True(host.Services.GetRequiredService<IHttpsConfigurationService>().IsInitialized);
207+
}
208+
186209
[Fact]
187210
public async Task UseHttpsJustWorks()
188211
{

src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,56 @@ void CheckListenOptions(X509Certificate2 expectedCert)
384384
}
385385
}
386386

387+
[Fact]
388+
public void HasDefaultOrDevelopmentCertificate_DevelopmentCertificate()
389+
{
390+
try
391+
{
392+
var serverOptions = CreateServerOptions();
393+
var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
394+
var bytes = certificate.Export(X509ContentType.Pkcs12, "1234");
395+
var path = GetCertificatePath();
396+
Directory.CreateDirectory(Path.GetDirectoryName(path));
397+
File.WriteAllBytes(path, bytes);
398+
399+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
400+
{
401+
new KeyValuePair<string, string>("Certificates:Development:Password", "1234"),
402+
}).Build();
403+
404+
serverOptions.Configure(config);
405+
serverOptions.ConfigurationLoader.Load();
406+
407+
Assert.True(serverOptions.HasDefaultOrDevelopmentCertificate);
408+
}
409+
finally
410+
{
411+
if (File.Exists(GetCertificatePath()))
412+
{
413+
File.Delete(GetCertificatePath());
414+
}
415+
}
416+
}
417+
418+
[Fact]
419+
public void HasDefaultOrDevelopmentCertificate_DefaultCertificate()
420+
{
421+
var certificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword", X509KeyStorageFlags.Exportable);
422+
423+
var serverOptions = CreateServerOptions();
424+
425+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
426+
{
427+
new KeyValuePair<string, string>("Certificates:Default:Path", TestResources.TestCertificatePath),
428+
new KeyValuePair<string, string>("Certificates:Default:Password", "testPassword")
429+
}).Build();
430+
431+
serverOptions.Configure(config);
432+
serverOptions.ConfigurationLoader.Load();
433+
434+
Assert.True(serverOptions.HasDefaultOrDevelopmentCertificate);
435+
}
436+
387437
[Fact]
388438
public void ConfigureEndpoint_ThrowsWhen_The_PasswordIsMissing()
389439
{

src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,59 @@ await sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
775775
Assert.True(onAuthenticateCalled, "onAuthenticateCalled");
776776
}
777777

778+
[Fact]
779+
public void HasDefaultOrDevelopmentCertificate_ConfigurationNotLoaded()
780+
{
781+
var serverOptions = CreateServerOptions();
782+
serverOptions.TestOverrideDefaultCertificate = _x509Certificate2;
783+
784+
serverOptions.Configure();
785+
786+
Assert.Throws<InvalidOperationException>(() => serverOptions.HasDefaultOrDevelopmentCertificate);
787+
}
788+
789+
[Fact]
790+
public void HasDefaultOrDevelopmentCertificate_ConfigurationLoaded()
791+
{
792+
var serverOptions = CreateServerOptions();
793+
serverOptions.TestOverrideDefaultCertificate = _x509Certificate2;
794+
795+
serverOptions.Configure();
796+
serverOptions.ConfigurationLoader.Load();
797+
798+
Assert.True(serverOptions.HasDefaultOrDevelopmentCertificate);
799+
}
800+
801+
[Fact]
802+
public void HasDefaultOrDevelopmentCertificate_NoConfiguration()
803+
{
804+
var serverOptions = CreateServerOptions();
805+
serverOptions.TestOverrideDefaultCertificate = _x509Certificate2;
806+
807+
Assert.True(serverOptions.HasDefaultOrDevelopmentCertificate);
808+
}
809+
810+
[Fact]
811+
public void HasDefaultOrDevelopmentCertificate_NoCertificate()
812+
{
813+
var serverOptions = CreateServerOptions();
814+
serverOptions.IsDevelopmentCertificateLoaded = true; // Prevent the system default from being loaded
815+
816+
Assert.False(serverOptions.HasDefaultOrDevelopmentCertificate);
817+
}
818+
819+
[Fact]
820+
public void HasDefaultOrDevelopmentCertificate_FromHttpsDefaults()
821+
{
822+
var serverOptions = CreateServerOptions();
823+
serverOptions.ConfigureHttpsDefaults(httpsOptions =>
824+
{
825+
httpsOptions.ServerCertificate = _x509Certificate2;
826+
});
827+
828+
Assert.True(serverOptions.HasDefaultOrDevelopmentCertificate);
829+
}
830+
778831
private class HandshakeErrorLoggerProvider : ILoggerProvider
779832
{
780833
public HttpsConnectionFilterLogger FilterLogger { get; set; } = new HttpsConnectionFilterLogger();

0 commit comments

Comments
 (0)