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:
Javier Calvarro Nelson 2018-01-08 10:28:38 -08:00
parent 1bce01cb9c
commit e6bb551018
5 changed files with 212 additions and 4 deletions

View File

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

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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.