diff --git a/src/Kestrel.Core/Internal/KestrelServerOptionsSetup.cs b/src/Kestrel.Core/Internal/KestrelServerOptionsSetup.cs index 18831f0b49..013ce0ac80 100644 --- a/src/Kestrel.Core/Internal/KestrelServerOptionsSetup.cs +++ b/src/Kestrel.Core/Internal/KestrelServerOptionsSetup.cs @@ -29,10 +29,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal private void UseDefaultDeveloperCertificate(KestrelServerOptions options) { - var certificateManager = new CertificateManager(); - var certificate = certificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true) - .FirstOrDefault(); 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); diff --git a/src/Kestrel.Core/Internal/LoggerExtensions.cs b/src/Kestrel.Core/Internal/LoggerExtensions.cs index 1f3b8d131d..0a4c6f3a5e 100644 --- a/src/Kestrel.Core/Internal/LoggerExtensions.cs +++ b/src/Kestrel.Core/Internal/LoggerExtensions.cs @@ -13,8 +13,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal private static readonly Action _unableToLocateDevelopmentCertificate = LoggerMessage.Define(LogLevel.Debug, new EventId(1, nameof(UnableToLocateDevelopmentCertificate)), "Unable to locate an appropriate development https certificate."); + private static readonly Action _failedToLocateDevelopmentCertificateFile = + LoggerMessage.Define(LogLevel.Debug, new EventId(2, nameof(FailedToLocateDevelopmentCertificateFile)), "Failed to locate the development https certificate at '{certificatePath}'."); + + private static readonly Action _failedToLoadDevelopmentCertificate = + LoggerMessage.Define(LogLevel.Debug, new EventId(3, nameof(FailedToLoadDevelopmentCertificate)), "Failed to load the development https certificate at '{certificatePath}'."); + public static void LocatedDevelopmentCertificate(this ILogger logger, X509Certificate2 certificate) => _locatedDevelopmentCertificate(logger, certificate.Subject, certificate.Thumbprint, null); public static void UnableToLocateDevelopmentCertificate(this ILogger logger) => _unableToLocateDevelopmentCertificate(logger, null); + + public static void FailedToLocateDevelopmentCertificateFile(this ILogger logger, string certificatePath) => _failedToLocateDevelopmentCertificateFile(logger, certificatePath, null); + + public static void FailedToLoadDevelopmentCertificate(this ILogger logger, string certificatePath) => _failedToLoadDevelopmentCertificate(logger, certificatePath, null); } } diff --git a/src/Kestrel.Core/KestrelConfigurationLoader.cs b/src/Kestrel.Core/KestrelConfigurationLoader.cs index 1dfb9d262c..71337a11d2 100644 --- a/src/Kestrel.Core/KestrelConfigurationLoader.cs +++ b/src/Kestrel.Core/KestrelConfigurationLoader.cs @@ -6,14 +6,18 @@ using System.Collections.Generic; using System.IO; 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.Https; using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel { @@ -205,7 +209,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel var configReader = new ConfigurationReader(Configuration); LoadDefaultCert(configReader); - + foreach (var endpoint in configReader.Endpoints) { var listenOptions = AddressBinder.ParseAddress(endpoint.Url, out var https); @@ -259,6 +263,76 @@ namespace Microsoft.AspNetCore.Server.Kestrel Options.DefaultCertificate = defaultCert; } } + else + { + var logger = Options.ApplicationServices.GetRequiredService>(); + var certificate = FindDeveloperCertificateFile(configReader, logger); + if (certificate != null) + { + logger.LocatedDevelopmentCertificate(certificate); + Options.DefaultCertificate = certificate; + } + } + } + + private X509Certificate2 FindDeveloperCertificateFile(ConfigurationReader configReader, ILogger logger) + { + string certificatePath = null; + try + { + if (configReader.Certificates.TryGetValue("Development", out var certificateConfig) && + certificateConfig.Path == null && + certificateConfig.Password != null && + TryGetCertificatePath(out certificatePath) && + File.Exists(certificatePath)) + { + var certificate = new X509Certificate2(certificatePath, certificateConfig.Password); + return IsDevelopmentCertificate(certificate) ? certificate : null; + } + else if (!File.Exists(certificatePath)) + { + logger.FailedToLocateDevelopmentCertificateFile(certificatePath); + } + } + catch (CryptographicException) + { + logger.FailedToLoadDevelopmentCertificate(certificatePath); + } + + return null; + } + + private 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(out string path) + { + var hostingEnvironment = Options.ApplicationServices.GetRequiredService(); + var appName = hostingEnvironment.ApplicationName; + + // This will go away when we implement + // https://github.com/aspnet/Hosting/issues/1294 + var appData = Environment.GetEnvironmentVariable("APPDATA"); + var home = Environment.GetEnvironmentVariable("HOME"); + 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, $"{appName}.pfx") : null; + return path != null; } private X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName) diff --git a/test/Kestrel.Tests/KestrelConfigurationBuilderTests.cs b/test/Kestrel.Tests/KestrelConfigurationBuilderTests.cs index bdd9bff4c1..33caeb1d34 100644 --- a/test/Kestrel.Tests/KestrelConfigurationBuilderTests.cs +++ b/test/Kestrel.Tests/KestrelConfigurationBuilderTests.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Testing; @@ -22,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests var serverOptions = new KestrelServerOptions(); serverOptions.ApplicationServices = new ServiceCollection() .AddLogging() + .AddSingleton(new HostingEnvironment() { ApplicationName = "TestApplication" }) .BuildServiceProvider(); return serverOptions; } @@ -209,5 +212,117 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests Assert.NotNull(serverOptions.ListenOptions[0].ConnectionAdapters.Where(adapter => adapter.IsHttps).SingleOrDefault()); Assert.NotNull(serverOptions.ListenOptions[1].ConnectionAdapters.Where(adapter => adapter.IsHttps).SingleOrDefault()); } + + [Fact] + public void ConfigureEndpointDevelopmentCertificateGetsLoadedWhenPresent() + { + try + { + var serverOptions = CreateServerOptions(); + var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "aspnetdevcert", X509KeyStorageFlags.Exportable); + var bytes = certificate.Export(X509ContentType.Pkcs12, "1234"); + var path = GetCertificatePath(); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + File.WriteAllBytes(path, bytes); + + var ran1 = false; + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:End1:Url", "https://*:5001"), + new KeyValuePair("Certificates:Development:Password", "1234"), + }).Build(); + + serverOptions + .Configure(config) + .Endpoint("End1", opt => + { + ran1 = true; + Assert.True(opt.IsHttps); + Assert.Equal(opt.HttpsOptions.ServerCertificate.SerialNumber, certificate.SerialNumber); + }).Load(); + + Assert.True(ran1); + Assert.NotNull(serverOptions.DefaultCertificate); + } + finally + { + if (File.Exists(GetCertificatePath())) + { + File.Delete(GetCertificatePath()); + } + } + } + + [Fact] + public void ConfigureEndpointDevelopmentCertificateGetsIgnoredIfPasswordIsNotCorrect() + { + try + { + var serverOptions = CreateServerOptions(); + var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "aspnetdevcert", 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", "12341234"), + }).Build(); + + serverOptions + .Configure(config) + .Load(); + + Assert.Null(serverOptions.DefaultCertificate); + } + finally + { + if (File.Exists(GetCertificatePath())) + { + File.Delete(GetCertificatePath()); + } + } + } + + [Fact] + public void ConfigureEndpointDevelopmentCertificateGetsIgnoredIfPfxFileDoesNotExist() + { + try + { + var serverOptions = CreateServerOptions(); + if (File.Exists(GetCertificatePath())) + { + File.Delete(GetCertificatePath()); + } + + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Certificates:Development:Password", "12341234") + }).Build(); + + serverOptions + .Configure(config) + .Load(); + + Assert.Null(serverOptions.DefaultCertificate); + } + finally + { + if (File.Exists(GetCertificatePath())) + { + File.Delete(GetCertificatePath()); + } + } + } + + private static string GetCertificatePath() + { + var appData = Environment.GetEnvironmentVariable("APPDATA"); + var home = Environment.GetEnvironmentVariable("HOME"); + var basePath = appData != null ? Path.Combine(appData, "ASP.NET", "https") : null; + basePath = basePath ?? (home != null ? Path.Combine(home, ".aspnet", "https") : null); + return Path.Combine(basePath, $"TestApplication.pfx"); + } } } diff --git a/test/shared/TestCertificates/aspnetdevcert.pfx b/test/shared/TestCertificates/aspnetdevcert.pfx new file mode 100644 index 0000000000..e6eeeaa2e1 Binary files /dev/null and b/test/shared/TestCertificates/aspnetdevcert.pfx differ