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)
|
||||
{
|
||||
var certificateManager = new CertificateManager();
|
||||
var certificate = certificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true)
|
||||
.FirstOrDefault();
|
||||
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)
|
||||
{
|
||||
logger.LocatedDevelopmentCertificate(certificate);
|
||||
|
|
|
|||
|
|
@ -13,8 +13,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
|
|||
private static readonly Action<ILogger, Exception> _unableToLocateDevelopmentCertificate =
|
||||
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 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.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<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)
|
||||
|
|
|
|||
|
|
@ -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<IHostingEnvironment>(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<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