Adds support for loading the developer certificate from a pfx file
* If we can't find a developer certificate on the certificate store
we will look for a developer certificate on the file system if a
password has been specified for the Development certificate.
* We will look at ${APPDATA}/ASP.NET/https/<<AppName>>.pfx for windows
and fallback to ${HOME}/.aspnet/https/<<AppName>>.pfx
* In case the password wasn't specified through configuration, the file
is not found on the file system or can't be loaded, we won't
do anything.
This commit is contained in:
parent
1bce01cb9c
commit
e6bb551018
|
|
@ -29,10 +29,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
||||||
|
|
||||||
private void UseDefaultDeveloperCertificate(KestrelServerOptions options)
|
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<ILogger<KestrelServer>>();
|
var logger = options.ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
|
||||||
|
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)
|
if (certificate != null)
|
||||||
{
|
{
|
||||||
logger.LocatedDevelopmentCertificate(certificate);
|
logger.LocatedDevelopmentCertificate(certificate);
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
|
||||||
private static readonly Action<ILogger, Exception> _unableToLocateDevelopmentCertificate =
|
private static readonly Action<ILogger, Exception> _unableToLocateDevelopmentCertificate =
|
||||||
LoggerMessage.Define(LogLevel.Debug, new EventId(1, nameof(UnableToLocateDevelopmentCertificate)), "Unable to locate an appropriate development https certificate.");
|
LoggerMessage.Define(LogLevel.Debug, new EventId(1, nameof(UnableToLocateDevelopmentCertificate)), "Unable to locate an appropriate development https certificate.");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, string, Exception> _failedToLocateDevelopmentCertificateFile =
|
||||||
|
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(2, nameof(FailedToLocateDevelopmentCertificateFile)), "Failed to locate the development https certificate at '{certificatePath}'.");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, string, Exception> _failedToLoadDevelopmentCertificate =
|
||||||
|
LoggerMessage.Define<string>(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 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 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,18 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using Microsoft.AspNetCore.Certificates.Generation;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
|
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Internal;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel
|
namespace Microsoft.AspNetCore.Server.Kestrel
|
||||||
{
|
{
|
||||||
|
|
@ -259,6 +263,76 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
||||||
Options.DefaultCertificate = defaultCert;
|
Options.DefaultCertificate = defaultCert;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var logger = Options.ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
|
||||||
|
var certificate = FindDeveloperCertificateFile(configReader, logger);
|
||||||
|
if (certificate != null)
|
||||||
|
{
|
||||||
|
logger.LocatedDevelopmentCertificate(certificate);
|
||||||
|
Options.DefaultCertificate = certificate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private X509Certificate2 FindDeveloperCertificateFile(ConfigurationReader configReader, ILogger<KestrelServer> 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<IHostingEnvironment>();
|
||||||
|
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)
|
private X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName)
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Hosting.Internal;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
|
@ -22,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests
|
||||||
var serverOptions = new KestrelServerOptions();
|
var serverOptions = new KestrelServerOptions();
|
||||||
serverOptions.ApplicationServices = new ServiceCollection()
|
serverOptions.ApplicationServices = new ServiceCollection()
|
||||||
.AddLogging()
|
.AddLogging()
|
||||||
|
.AddSingleton<IHostingEnvironment>(new HostingEnvironment() { ApplicationName = "TestApplication" })
|
||||||
.BuildServiceProvider();
|
.BuildServiceProvider();
|
||||||
return serverOptions;
|
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[0].ConnectionAdapters.Where(adapter => adapter.IsHttps).SingleOrDefault());
|
||||||
Assert.NotNull(serverOptions.ListenOptions[1].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<string, string>("Endpoints:End1:Url", "https://*:5001"),
|
||||||
|
new KeyValuePair<string, string>("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<string, string>("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<string, string>("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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
Loading…
Reference in New Issue