Show fwlink on HTTPS certificate errors (#83).

This commit is contained in:
Cesar Blum Silveira 2017-05-03 15:55:19 -07:00 committed by GitHub
parent 4d5e1076c9
commit 012d9990ef
4 changed files with 263 additions and 169 deletions

View File

@ -13,7 +13,7 @@ namespace AppSettings
{ {
using (WebHost.Start(context => context.Response.WriteAsync("Hello, World!"))) using (WebHost.Start(context => context.Response.WriteAsync("Hello, World!")))
{ {
Console.WriteLine("Running application: Press any key to shutdown..."); Console.WriteLine("Running application: Press any key to shutdown.");
Console.ReadKey(); Console.ReadKey();
} }
} }

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore namespace Microsoft.AspNetCore
{ {
@ -15,14 +16,17 @@ namespace Microsoft.AspNetCore
public class CertificateLoader public class CertificateLoader
{ {
private readonly IConfiguration _certificatesConfiguration; private readonly IConfiguration _certificatesConfiguration;
private readonly string _environmentName;
private readonly ICertificateFileLoader _certificateFileLoader; private readonly ICertificateFileLoader _certificateFileLoader;
private readonly ICertificateStoreLoader _certificateStoreLoader; private readonly ICertificateStoreLoader _certificateStoreLoader;
private readonly ILogger _logger;
/// <summary> /// <summary>
/// Creates a new instance of <see cref="CertificateLoader"/>. /// Creates a new instance of <see cref="CertificateLoader"/> that can load certificate references from configuration.
/// </summary> /// </summary>
public CertificateLoader() /// <param name="certificatesConfiguration">An <see cref="IConfiguration"/> with information about certificates.</param>
: this(null) public CertificateLoader(IConfiguration certificatesConfiguration)
: this(certificatesConfiguration, null, null)
{ {
} }
@ -30,17 +34,35 @@ namespace Microsoft.AspNetCore
/// Creates a new instance of <see cref="CertificateLoader"/> that can load certificate references from configuration. /// Creates a new instance of <see cref="CertificateLoader"/> that can load certificate references from configuration.
/// </summary> /// </summary>
/// <param name="certificatesConfiguration">An <see cref="IConfiguration"/> with information about certificates.</param> /// <param name="certificatesConfiguration">An <see cref="IConfiguration"/> with information about certificates.</param>
public CertificateLoader(IConfiguration certificatesConfiguration) /// <param name="loggerFactory">An <see cref="ILoggerFactory"/> instance.</param>
: this(certificatesConfiguration, new CertificateFileLoader(), new CertificateStoreLoader()) public CertificateLoader(IConfiguration certificatesConfiguration, ILoggerFactory loggerFactory)
: this(certificatesConfiguration, loggerFactory, null)
{ {
_certificatesConfiguration = certificatesConfiguration;
} }
internal CertificateLoader(IConfiguration certificatesConfiguration, ICertificateFileLoader certificateFileLoader, ICertificateStoreLoader certificateStoreLoader) /// <summary>
/// Creates a new instance of <see cref="CertificateLoader"/> that can load certificate references from configuration.
/// </summary>
/// <param name="certificatesConfiguration">An <see cref="IConfiguration"/> with information about certificates.</param>
/// <param name="loggerFactory">An <see cref="ILoggerFactory"/> instance.</param>
/// <param name="environmentName">The name of the environment the application is running in.</param>
public CertificateLoader(IConfiguration certificatesConfiguration, ILoggerFactory loggerFactory, string environmentName)
: this(certificatesConfiguration, loggerFactory, environmentName, new CertificateFileLoader(), new CertificateStoreLoader())
{ {
}
internal CertificateLoader(
IConfiguration certificatesConfiguration,
ILoggerFactory loggerFactory,
string environmentName,
ICertificateFileLoader certificateFileLoader,
ICertificateStoreLoader certificateStoreLoader)
{
_environmentName = environmentName;
_certificatesConfiguration = certificatesConfiguration; _certificatesConfiguration = certificatesConfiguration;
_certificateFileLoader = certificateFileLoader; _certificateFileLoader = certificateFileLoader;
_certificateStoreLoader = certificateStoreLoader; _certificateStoreLoader = certificateStoreLoader;
_logger = loggerFactory?.CreateLogger("Microsoft.AspNetCore.CertificateLoader");
} }
/// <summary> /// <summary>
@ -99,7 +121,8 @@ namespace Microsoft.AspNetCore
if (!certificateConfiguration.Exists()) if (!certificateConfiguration.Exists())
{ {
throw new InvalidOperationException($"No certificate named {certificateName} found in configuration"); var environmentName = _environmentName != null ? $" ({_environmentName})" : "";
throw new KeyNotFoundException($"No certificate named '{certificateName}' found in configuration for the current environment{environmentName}.");
} }
return LoadSingle(certificateConfiguration); return LoadSingle(certificateConfiguration);
@ -116,10 +139,10 @@ namespace Microsoft.AspNetCore
certificateSource = new CertificateFileSource(_certificateFileLoader); certificateSource = new CertificateFileSource(_certificateFileLoader);
break; break;
case "store": case "store":
certificateSource = new CertificateStoreSource(_certificateStoreLoader); certificateSource = new CertificateStoreSource(_certificateStoreLoader, _logger);
break; break;
default: default:
throw new InvalidOperationException($"Invalid certificate source kind: {sourceKind}"); throw new InvalidOperationException($"Invalid certificate source kind '{sourceKind}'.");
} }
certificateConfiguration.Bind(certificateSource); certificateConfiguration.Bind(certificateSource);
@ -163,7 +186,7 @@ namespace Microsoft.AspNetCore
if (error != null) if (error != null)
{ {
throw error; throw new InvalidOperationException($"Unable to load certificate from file '{Path}'. Error details: '{error.Message}'.", error);
} }
return certificate; return certificate;
@ -188,10 +211,12 @@ namespace Microsoft.AspNetCore
private class CertificateStoreSource : CertificateSource private class CertificateStoreSource : CertificateSource
{ {
private readonly ICertificateStoreLoader _certificateStoreLoader; private readonly ICertificateStoreLoader _certificateStoreLoader;
private readonly ILogger _logger;
public CertificateStoreSource(ICertificateStoreLoader certificateStoreLoader) public CertificateStoreSource(ICertificateStoreLoader certificateStoreLoader, ILogger logger)
{ {
_certificateStoreLoader = certificateStoreLoader; _certificateStoreLoader = certificateStoreLoader;
_logger = logger;
} }
public string Subject { get; set; } public string Subject { get; set; }
@ -203,10 +228,17 @@ namespace Microsoft.AspNetCore
{ {
if (!Enum.TryParse(StoreLocation, ignoreCase: true, result: out StoreLocation storeLocation)) if (!Enum.TryParse(StoreLocation, ignoreCase: true, result: out StoreLocation storeLocation))
{ {
throw new InvalidOperationException($"Invalid store location: {StoreLocation}"); throw new InvalidOperationException($"The certificate store location '{StoreLocation}' is invalid.");
} }
return _certificateStoreLoader.Load(Subject, StoreName, storeLocation, !AllowInvalid); var certificate = _certificateStoreLoader.Load(Subject, StoreName, storeLocation, !AllowInvalid);
if (certificate == null)
{
_logger?.LogWarning($"Unable to find a matching certificate for subject '{Subject}' in store '{StoreName}' in '{StoreLocation}'.");
}
return certificate;
} }
} }
} }

View File

@ -4,20 +4,29 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore namespace Microsoft.AspNetCore
{ {
internal class KestrelServerOptionsSetup : IConfigureOptions<KestrelServerOptions> internal class KestrelServerOptionsSetup : IConfigureOptions<KestrelServerOptions>
{ {
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IConfiguration _configurationRoot; private readonly IConfiguration _configurationRoot;
private readonly ILoggerFactory _loggerFactory;
public KestrelServerOptionsSetup(IConfiguration configurationRoot) public KestrelServerOptionsSetup(
IHostingEnvironment hostingEnvironment,
IConfiguration configurationRoot,
ILoggerFactory loggerFactory)
{ {
_hostingEnvironment = hostingEnvironment;
_configurationRoot = configurationRoot; _configurationRoot = configurationRoot;
_loggerFactory = loggerFactory;
} }
public void Configure(KestrelServerOptions options) public void Configure(KestrelServerOptions options)
@ -27,7 +36,7 @@ namespace Microsoft.AspNetCore
private void BindConfiguration(KestrelServerOptions options) private void BindConfiguration(KestrelServerOptions options)
{ {
var certificateLoader = new CertificateLoader(_configurationRoot.GetSection("Certificates")); var certificateLoader = new CertificateLoader(_configurationRoot.GetSection("Certificates"), _loggerFactory, _hostingEnvironment.EnvironmentName);
foreach (var endPoint in _configurationRoot.GetSection("Kestrel:EndPoints").GetChildren()) foreach (var endPoint in _configurationRoot.GetSection("Kestrel:EndPoints").GetChildren())
{ {
@ -56,14 +65,22 @@ namespace Microsoft.AspNetCore
options.Listen(address, port, listenOptions => options.Listen(address, port, listenOptions =>
{ {
var certificateConfig = endPoint.GetSection("Certificate"); var certificateConfig = endPoint.GetSection("Certificate");
X509Certificate2 certificate;
if (certificateConfig.Exists()) if (certificateConfig.Exists())
{ {
var certificate = certificateLoader.Load(certificateConfig).FirstOrDefault(); try
if (certificate == null)
{ {
throw new InvalidOperationException($"Unable to load certificate for endpoint '{endPoint.Key}'"); certificate = certificateLoader.Load(certificateConfig).FirstOrDefault();
if (certificate == null)
{
throw new InvalidOperationException($"No certificate found for endpoint '{endPoint.Key}'.");
}
}
catch (Exception ex)
{
throw new InvalidOperationException("Unable to configure HTTPS endpoint. For information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054.", ex);
} }
listenOptions.UseHttps(certificate); listenOptions.UseHttps(certificate);

View File

@ -6,8 +6,9 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Xunit; using Microsoft.Extensions.Logging;
using Moq; using Moq;
using Xunit;
namespace Microsoft.AspNetCore.FunctionalTests namespace Microsoft.AspNetCore.FunctionalTests
{ {
@ -35,6 +36,8 @@ namespace Microsoft.AspNetCore.FunctionalTests
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"), configuration.GetSection("Certificates"),
null,
null,
certificateFileLoader.Object, certificateFileLoader.Object,
Mock.Of<ICertificateStoreLoader>()); Mock.Of<ICertificateStoreLoader>());
@ -45,31 +48,28 @@ namespace Microsoft.AspNetCore.FunctionalTests
} }
[Fact] [Fact]
public void Throws_SingleCertificateName_File_KeyNotFound() public void Throws_SingleCertificateName_KeyNotFound()
{ {
var configuration = new ConfigurationBuilder() var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> .AddInMemoryCollection(new Dictionary<string, string>
{ {
["Certificates:Certificate1:Source"] = "File", ["TestConfig:Certificate"] = "Certificate1"
["Certificates:Certificate1:Path"] = "Certificate1.pfx",
["Certificates:Certificate1:Password"] = "Password1",
["TestConfig:Certificate"] = "Certificate2"
}) })
.Build(); .Build();
var certificate = new X509Certificate2();
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"), configuration.GetSection("Certificates"),
null,
null,
Mock.Of<ICertificateFileLoader>(), Mock.Of<ICertificateFileLoader>(),
Mock.Of<ICertificateStoreLoader>()); Mock.Of<ICertificateStoreLoader>());
var exception = Assert.Throws<InvalidOperationException>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"))); var exception = Assert.Throws<KeyNotFoundException>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")));
Assert.Equal("No certificate named Certificate2 found in configuration", exception.Message); Assert.Equal("No certificate named 'Certificate1' found in configuration for the current environment.", exception.Message);
} }
[Fact] [Fact]
public void Throws_SingleCertificateName_File_FileNotFound() public void Throws_SingleCertificateName_File_FileLoadError()
{ {
var configuration = new ConfigurationBuilder() var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> .AddInMemoryCollection(new Dictionary<string, string>
@ -81,19 +81,20 @@ namespace Microsoft.AspNetCore.FunctionalTests
}) })
.Build(); .Build();
var exception = new Exception();
var certificateFileLoader = new Mock<ICertificateFileLoader>(); var certificateFileLoader = new Mock<ICertificateFileLoader>();
certificateFileLoader certificateFileLoader
.Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny<X509KeyStorageFlags>())) .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny<X509KeyStorageFlags>()))
.Callback(() => throw exception); .Callback(() => throw new Exception(nameof(Throws_SingleCertificateName_File_FileLoadError)));
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"), configuration.GetSection("Certificates"),
null,
null,
certificateFileLoader.Object, certificateFileLoader.Object,
Mock.Of<ICertificateStoreLoader>()); Mock.Of<ICertificateStoreLoader>());
Assert.Same(exception, Assert.Throws<Exception>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")))); var exception = Assert.Throws<InvalidOperationException>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")));
Assert.Equal($"Unable to load certificate from file 'Certificate1.pfx'. Error details: '{nameof(Throws_SingleCertificateName_File_FileLoadError)}'.", exception.Message);
} }
[Fact] [Fact]
@ -119,6 +120,8 @@ namespace Microsoft.AspNetCore.FunctionalTests
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"), configuration.GetSection("Certificates"),
null,
null,
Mock.Of<ICertificateFileLoader>(), Mock.Of<ICertificateFileLoader>(),
certificateStoreLoader.Object); certificateStoreLoader.Object);
@ -127,30 +130,7 @@ namespace Microsoft.AspNetCore.FunctionalTests
Assert.Same(certificate, loadedCertificates.ElementAt(0)); Assert.Same(certificate, loadedCertificates.ElementAt(0));
certificateStoreLoader.VerifyAll(); certificateStoreLoader.VerifyAll();
} }
[Fact]
public void Throws_SingleCertificateName_Store_KeyNotFound()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
["Certificates:Certificate1:Source"] = "Store",
["Certificates:Certificate1:Subject"] = "localhost",
["Certificates:Certificate1:StoreName"] = "My",
["Certificates:Certificate1:StoreLocation"] = "CurrentUser",
["TestConfig:Certificate"] = "Certificate2"
})
.Build();
var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"),
Mock.Of<ICertificateFileLoader>(),
Mock.Of<ICertificateStoreLoader>());
var exception = Assert.Throws<InvalidOperationException>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")));
Assert.Equal("No certificate named Certificate2 found in configuration", exception.Message);
}
[Fact] [Fact]
public void ReturnsNull_SingleCertificateName_Store_NotFoundInStore() public void ReturnsNull_SingleCertificateName_Store_NotFoundInStore()
{ {
@ -172,6 +152,8 @@ namespace Microsoft.AspNetCore.FunctionalTests
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"), configuration.GetSection("Certificates"),
null,
null,
Mock.Of<ICertificateFileLoader>(), Mock.Of<ICertificateFileLoader>(),
certificateStoreLoader.Object); certificateStoreLoader.Object);
@ -209,6 +191,8 @@ namespace Microsoft.AspNetCore.FunctionalTests
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"), configuration.GetSection("Certificates"),
null,
null,
certificateFileLoader.Object, certificateFileLoader.Object,
Mock.Of<ICertificateStoreLoader>()); Mock.Of<ICertificateStoreLoader>());
@ -220,12 +204,9 @@ namespace Microsoft.AspNetCore.FunctionalTests
} }
[Theory] [Theory]
[InlineData("Certificate1;NotFound")] [InlineData("Certificate1;Certificate2")]
[InlineData("Certificate1;Certificate2;NotFound")] [InlineData("Certificate2;Certificate1")]
[InlineData("NotFound;Certificate1")] public void Throws_MultipleCertificateNames_File_FileLoadError(string certificateNames)
[InlineData("NotFound;Certificate1;Certificate2")]
[InlineData("Certificate1;NotFound;Certificate2")]
public void Throws_MultipleCertificateNames_File_KeyNotFound(string certificateNames)
{ {
var configuration = new ConfigurationBuilder() var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> .AddInMemoryCollection(new Dictionary<string, string>
@ -241,7 +222,6 @@ namespace Microsoft.AspNetCore.FunctionalTests
.Build(); .Build();
var certificate1 = new X509Certificate2(); var certificate1 = new X509Certificate2();
var certificate2 = new X509Certificate2();
var certificateFileLoader = new Mock<ICertificateFileLoader>(); var certificateFileLoader = new Mock<ICertificateFileLoader>();
certificateFileLoader certificateFileLoader
@ -249,52 +229,17 @@ namespace Microsoft.AspNetCore.FunctionalTests
.Returns(certificate1); .Returns(certificate1);
certificateFileLoader certificateFileLoader
.Setup(loader => loader.Load("Certificate2.pfx", "Password2", It.IsAny<X509KeyStorageFlags>())) .Setup(loader => loader.Load("Certificate2.pfx", "Password2", It.IsAny<X509KeyStorageFlags>()))
.Returns(certificate2); .Throws(new Exception(nameof(Throws_MultipleCertificateNames_File_FileLoadError)));
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"), configuration.GetSection("Certificates"),
null,
null,
certificateFileLoader.Object, certificateFileLoader.Object,
Mock.Of<ICertificateStoreLoader>()); Mock.Of<ICertificateStoreLoader>());
var exception = Assert.Throws<InvalidOperationException>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"))); var exception = Assert.Throws<InvalidOperationException>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")));
Assert.Equal("No certificate named NotFound found in configuration", exception.Message); Assert.Equal($"Unable to load certificate from file 'Certificate2.pfx'. Error details: '{nameof(Throws_MultipleCertificateNames_File_FileLoadError)}'.", exception.Message);
}
[Theory]
[InlineData("Certificate1;Certificate2")]
[InlineData("Certificate2;Certificate1")]
public void Throws_MultipleCertificateNames_File_FileNotFound(string certificateNames)
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
["Certificates:Certificate1:Source"] = "File",
["Certificates:Certificate1:Path"] = "Certificate1.pfx",
["Certificates:Certificate1:Password"] = "Password1",
["Certificates:Certificate2:Source"] = "File",
["Certificates:Certificate2:Path"] = "Certificate2.pfx",
["Certificates:Certificate2:Password"] = "Password2",
["TestConfig:Certificate"] = certificateNames
})
.Build();
var certificate1 = new X509Certificate2();
var exception = new Exception();
var certificateFileLoader = new Mock<ICertificateFileLoader>();
certificateFileLoader
.Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny<X509KeyStorageFlags>()))
.Returns(certificate1);
certificateFileLoader
.Setup(loader => loader.Load("Certificate2.pfx", "Password2", It.IsAny<X509KeyStorageFlags>()))
.Throws(exception);
var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"),
certificateFileLoader.Object,
Mock.Of<ICertificateStoreLoader>());
Assert.Same(exception, Assert.Throws<Exception>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"))));
} }
[Fact] [Fact]
@ -328,6 +273,8 @@ namespace Microsoft.AspNetCore.FunctionalTests
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"), configuration.GetSection("Certificates"),
null,
null,
Mock.Of<ICertificateFileLoader>(), Mock.Of<ICertificateFileLoader>(),
certificateStoreLoader.Object); certificateStoreLoader.Object);
@ -338,49 +285,6 @@ namespace Microsoft.AspNetCore.FunctionalTests
certificateStoreLoader.VerifyAll(); certificateStoreLoader.VerifyAll();
} }
[Theory]
[InlineData("Certificate1;NotFound")]
[InlineData("Certificate1;Certificate2;NotFound")]
[InlineData("NotFound;Certificate1")]
[InlineData("NotFound;Certificate1;Certificate2")]
[InlineData("Certificate1;NotFound;Certificate2")]
public void Throws_MultipleCertificateNames_Store_KeyNotFound(string certificateNames)
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
["Certificates:Certificate1:Source"] = "Store",
["Certificates:Certificate1:Subject"] = "localhost",
["Certificates:Certificate1:StoreName"] = "My",
["Certificates:Certificate1:StoreLocation"] = "CurrentUser",
["Certificates:Certificate2:Source"] = "Store",
["Certificates:Certificate2:Subject"] = "example.com",
["Certificates:Certificate2:StoreName"] = "Root",
["Certificates:Certificate2:StoreLocation"] = "LocalMachine",
["TestConfig:Certificate"] = certificateNames
})
.Build();
var certificate1 = new X509Certificate2();
var certificate2 = new X509Certificate2();
var certificateStoreLoader = new Mock<ICertificateStoreLoader>();
certificateStoreLoader
.Setup(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny<bool>()))
.Returns(certificate1);
certificateStoreLoader
.Setup(loader => loader.Load("example.com", "Root", StoreLocation.LocalMachine, It.IsAny<bool>()))
.Returns(certificate2);
var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"),
Mock.Of<ICertificateFileLoader>(),
certificateStoreLoader.Object);
var exception = Assert.Throws<InvalidOperationException>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")));
Assert.Equal("No certificate named NotFound found in configuration", exception.Message);
}
[Theory] [Theory]
[InlineData("Certificate1;Certificate2", 1)] [InlineData("Certificate1;Certificate2", 1)]
[InlineData("Certificate2;Certificate1", 1)] [InlineData("Certificate2;Certificate1", 1)]
@ -421,6 +325,8 @@ namespace Microsoft.AspNetCore.FunctionalTests
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"), configuration.GetSection("Certificates"),
null,
null,
Mock.Of<ICertificateFileLoader>(), Mock.Of<ICertificateFileLoader>(),
certificateStoreLoader.Object); certificateStoreLoader.Object);
@ -460,6 +366,8 @@ namespace Microsoft.AspNetCore.FunctionalTests
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"), configuration.GetSection("Certificates"),
null,
null,
certificateFileLoader.Object, certificateFileLoader.Object,
certificateStoreLoader.Object); certificateStoreLoader.Object);
@ -475,7 +383,7 @@ namespace Microsoft.AspNetCore.FunctionalTests
[InlineData("Certificate1;Certificate2;NotFound")] [InlineData("Certificate1;Certificate2;NotFound")]
[InlineData("Certificate1;NotFound;Certificate2")] [InlineData("Certificate1;NotFound;Certificate2")]
[InlineData("NotFound;Certificate1;Certificate2")] [InlineData("NotFound;Certificate1;Certificate2")]
public void Throws_MultipleCertificateNames_FileAndStore_KeyNotFound(string certificateNames) public void Throws_MultipleCertificateNames_KeyNotFound(string certificateNames)
{ {
var configuration = new ConfigurationBuilder() var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> .AddInMemoryCollection(new Dictionary<string, string>
@ -506,17 +414,19 @@ namespace Microsoft.AspNetCore.FunctionalTests
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"), configuration.GetSection("Certificates"),
null,
null,
certificateFileLoader.Object, certificateFileLoader.Object,
certificateStoreLoader.Object); certificateStoreLoader.Object);
var exception = Assert.Throws<InvalidOperationException>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"))); var exception = Assert.Throws<KeyNotFoundException>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")));
Assert.Equal("No certificate named NotFound found in configuration", exception.Message); Assert.Equal("No certificate named 'NotFound' found in configuration for the current environment.", exception.Message);
} }
[Theory] [Theory]
[InlineData("Certificate1;Certificate2")] [InlineData("Certificate1;Certificate2")]
[InlineData("Certificate2;Certificate1")] [InlineData("Certificate2;Certificate1")]
public void Throws_MultipleCertificateNames_FileAndStore_FileNotFound(string certificateNames) public void Throws_MultipleCertificateNames_FileAndStore_FileLoadError(string certificateNames)
{ {
var configuration = new ConfigurationBuilder() var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> .AddInMemoryCollection(new Dictionary<string, string>
@ -532,13 +442,12 @@ namespace Microsoft.AspNetCore.FunctionalTests
}) })
.Build(); .Build();
var exception = new Exception();
var storeCertificate = new X509Certificate2(); var storeCertificate = new X509Certificate2();
var certificateFileLoader = new Mock<ICertificateFileLoader>(); var certificateFileLoader = new Mock<ICertificateFileLoader>();
certificateFileLoader certificateFileLoader
.Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny<X509KeyStorageFlags>())) .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny<X509KeyStorageFlags>()))
.Throws(exception); .Throws(new Exception(nameof(Throws_MultipleCertificateNames_FileAndStore_FileLoadError)));
var certificateStoreLoader = new Mock<ICertificateStoreLoader>(); var certificateStoreLoader = new Mock<ICertificateStoreLoader>();
certificateStoreLoader certificateStoreLoader
@ -547,10 +456,13 @@ namespace Microsoft.AspNetCore.FunctionalTests
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"), configuration.GetSection("Certificates"),
null,
null,
certificateFileLoader.Object, certificateFileLoader.Object,
certificateStoreLoader.Object); certificateStoreLoader.Object);
Assert.Same(exception, Assert.Throws<Exception>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")))); var exception = Assert.Throws<InvalidOperationException>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")));
Assert.Equal($"Unable to load certificate from file 'Certificate1.pfx'. Error details: '{nameof(Throws_MultipleCertificateNames_FileAndStore_FileLoadError)}'.", exception.Message);
} }
[Theory] [Theory]
@ -586,6 +498,8 @@ namespace Microsoft.AspNetCore.FunctionalTests
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"), configuration.GetSection("Certificates"),
null,
null,
certificateFileLoader.Object, certificateFileLoader.Object,
certificateStoreLoader.Object); certificateStoreLoader.Object);
@ -616,6 +530,8 @@ namespace Microsoft.AspNetCore.FunctionalTests
.Returns(certificate); .Returns(certificate);
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
null,
null,
null, null,
certificateFileLoader.Object, certificateFileLoader.Object,
Mock.Of<ICertificateStoreLoader>()); Mock.Of<ICertificateStoreLoader>());
@ -627,7 +543,7 @@ namespace Microsoft.AspNetCore.FunctionalTests
} }
[Fact] [Fact]
public void Throws_SingleCertificateInline_FileNotFound() public void Throws_SingleCertificateInline_FileLoadError()
{ {
var configuration = new ConfigurationBuilder() var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> .AddInMemoryCollection(new Dictionary<string, string>
@ -638,19 +554,20 @@ namespace Microsoft.AspNetCore.FunctionalTests
}) })
.Build(); .Build();
var exception = new Exception();
var certificateFileLoader = new Mock<ICertificateFileLoader>(); var certificateFileLoader = new Mock<ICertificateFileLoader>();
certificateFileLoader certificateFileLoader
.Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny<X509KeyStorageFlags>())) .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny<X509KeyStorageFlags>()))
.Throws(exception); .Throws(new Exception(nameof(Throws_SingleCertificateInline_FileLoadError)));
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
null,
null,
null, null,
certificateFileLoader.Object, certificateFileLoader.Object,
Mock.Of<ICertificateStoreLoader>()); Mock.Of<ICertificateStoreLoader>());
Assert.Same(exception, Assert.Throws<Exception>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")))); var exception = Assert.Throws<InvalidOperationException>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")));
Assert.Equal($"Unable to load certificate from file 'Certificate1.pfx'. Error details: '{nameof(Throws_SingleCertificateInline_FileLoadError)}'.", exception.Message);
certificateFileLoader.VerifyAll(); certificateFileLoader.VerifyAll();
} }
@ -675,6 +592,8 @@ namespace Microsoft.AspNetCore.FunctionalTests
.Returns(certificate); .Returns(certificate);
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
null,
null,
null, null,
Mock.Of<ICertificateFileLoader>(), Mock.Of<ICertificateFileLoader>(),
certificateStoreLoader.Object); certificateStoreLoader.Object);
@ -701,6 +620,8 @@ namespace Microsoft.AspNetCore.FunctionalTests
var certificateStoreLoader = new Mock<ICertificateStoreLoader>(); var certificateStoreLoader = new Mock<ICertificateStoreLoader>();
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
null,
null,
null, null,
Mock.Of<ICertificateFileLoader>(), Mock.Of<ICertificateFileLoader>(),
certificateStoreLoader.Object); certificateStoreLoader.Object);
@ -737,6 +658,8 @@ namespace Microsoft.AspNetCore.FunctionalTests
.Returns(certificate2); .Returns(certificate2);
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
null,
null,
null, null,
certificateFileLoader.Object, certificateFileLoader.Object,
Mock.Of<ICertificateStoreLoader>()); Mock.Of<ICertificateStoreLoader>());
@ -749,7 +672,7 @@ namespace Microsoft.AspNetCore.FunctionalTests
} }
[Fact] [Fact]
public void Throws_MultipleCertificatesInline_File_FileNotFound() public void Throws_MultipleCertificatesInline_File_FileLoadError()
{ {
var configuration = new ConfigurationBuilder() var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> .AddInMemoryCollection(new Dictionary<string, string>
@ -764,7 +687,6 @@ namespace Microsoft.AspNetCore.FunctionalTests
.Build(); .Build();
var certificate1 = new X509Certificate2(); var certificate1 = new X509Certificate2();
var exception = new Exception();
var certificateFileLoader = new Mock<ICertificateFileLoader>(); var certificateFileLoader = new Mock<ICertificateFileLoader>();
certificateFileLoader certificateFileLoader
@ -772,14 +694,17 @@ namespace Microsoft.AspNetCore.FunctionalTests
.Returns(certificate1); .Returns(certificate1);
certificateFileLoader certificateFileLoader
.Setup(loader => loader.Load("Certificate2.pfx", "Password2", It.IsAny<X509KeyStorageFlags>())) .Setup(loader => loader.Load("Certificate2.pfx", "Password2", It.IsAny<X509KeyStorageFlags>()))
.Throws(exception); .Throws(new Exception(nameof(Throws_MultipleCertificatesInline_File_FileLoadError)));
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
null,
null,
null, null,
certificateFileLoader.Object, certificateFileLoader.Object,
Mock.Of<ICertificateStoreLoader>()); Mock.Of<ICertificateStoreLoader>());
Assert.Same(exception, Assert.Throws<Exception>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificates")))); var exception = Assert.Throws<InvalidOperationException>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificates")));
Assert.Equal($"Unable to load certificate from file 'Certificate2.pfx'. Error details: '{nameof(Throws_MultipleCertificatesInline_File_FileLoadError)}'.", exception.Message);
} }
[Fact] [Fact]
@ -811,6 +736,8 @@ namespace Microsoft.AspNetCore.FunctionalTests
.Returns(certificate2); .Returns(certificate2);
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
null,
null,
null, null,
Mock.Of<ICertificateFileLoader>(), Mock.Of<ICertificateFileLoader>(),
certificateStoreLoader.Object); certificateStoreLoader.Object);
@ -851,6 +778,8 @@ namespace Microsoft.AspNetCore.FunctionalTests
.Returns(certificate); .Returns(certificate);
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
null,
null,
null, null,
Mock.Of<ICertificateFileLoader>(), Mock.Of<ICertificateFileLoader>(),
certificateStoreLoader.Object); certificateStoreLoader.Object);
@ -908,6 +837,8 @@ namespace Microsoft.AspNetCore.FunctionalTests
.Returns(storeCertificate2); .Returns(storeCertificate2);
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
null,
null,
null, null,
certificateFileLoader.Object, certificateFileLoader.Object,
certificateStoreLoader.Object); certificateStoreLoader.Object);
@ -922,7 +853,7 @@ namespace Microsoft.AspNetCore.FunctionalTests
} }
[Fact] [Fact]
public void Throws_MultipleCertificatesInline_FileAndStore_FileNotFound() public void Throws_MultipleCertificatesInline_FileAndStore_FileLoadError()
{ {
var configuration = new ConfigurationBuilder() var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> .AddInMemoryCollection(new Dictionary<string, string>
@ -937,13 +868,12 @@ namespace Microsoft.AspNetCore.FunctionalTests
}) })
.Build(); .Build();
var exception = new Exception();
var certificate = new X509Certificate2(); var certificate = new X509Certificate2();
var certificateFileLoader = new Mock<ICertificateFileLoader>(); var certificateFileLoader = new Mock<ICertificateFileLoader>();
certificateFileLoader certificateFileLoader
.Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny<X509KeyStorageFlags>())) .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny<X509KeyStorageFlags>()))
.Throws(exception); .Throws(new Exception(nameof(Throws_MultipleCertificatesInline_FileAndStore_FileLoadError)));
var certificateStoreLoader = new Mock<ICertificateStoreLoader>(); var certificateStoreLoader = new Mock<ICertificateStoreLoader>();
certificateStoreLoader certificateStoreLoader
@ -951,11 +881,14 @@ namespace Microsoft.AspNetCore.FunctionalTests
.Returns(certificate); .Returns(certificate);
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
null,
null,
null, null,
certificateFileLoader.Object, certificateFileLoader.Object,
certificateStoreLoader.Object); certificateStoreLoader.Object);
Assert.Same(exception, Assert.Throws<Exception>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificates")))); var exception = Assert.Throws<InvalidOperationException>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificates")));
Assert.Equal($"Unable to load certificate from file 'Certificate1.pfx'. Error details: '{nameof(Throws_MultipleCertificatesInline_FileAndStore_FileLoadError)}'.", exception.Message);
} }
[Fact] [Fact]
@ -987,6 +920,8 @@ namespace Microsoft.AspNetCore.FunctionalTests
.Returns<X509Certificate>(null); .Returns<X509Certificate>(null);
var certificateLoader = new CertificateLoader( var certificateLoader = new CertificateLoader(
null,
null,
null, null,
certificateFileLoader.Object, certificateFileLoader.Object,
certificateStoreLoader.Object); certificateStoreLoader.Object);
@ -995,5 +930,115 @@ namespace Microsoft.AspNetCore.FunctionalTests
Assert.Equal(1, loadedCertificates.Count()); Assert.Equal(1, loadedCertificates.Count());
Assert.Same(certificate, loadedCertificates.ElementAt(0)); Assert.Same(certificate, loadedCertificates.ElementAt(0));
} }
[Theory]
[InlineData("Development")]
[InlineData("Production")]
public void IncludesEnvironmentNameInExceptionWhenAvailable(string environmentName)
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
["TestConfig:Certificate"] = "Certificate1"
})
.Build();
var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"),
null,
environmentName,
Mock.Of<ICertificateFileLoader>(),
Mock.Of<ICertificateStoreLoader>());
var exception = Assert.Throws<KeyNotFoundException>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")));
Assert.Equal($"No certificate named 'Certificate1' found in configuration for the current environment ({environmentName}).", exception.Message);
}
[Fact]
public void DoesNotIncludeEnvironmentNameInExceptionWhenNotAvailable()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
["TestConfig:Certificate"] = "Certificate1"
})
.Build();
var certificateLoader = new CertificateLoader(
configuration.GetSection("Certificates"),
null,
null,
Mock.Of<ICertificateFileLoader>(),
Mock.Of<ICertificateStoreLoader>());
var exception = Assert.Throws<KeyNotFoundException>(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")));
Assert.Equal("No certificate named 'Certificate1' found in configuration for the current environment.", exception.Message);
}
[Fact]
public void WarningLoggedWhenCertificateNotFoundInStore()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
["TestConfig:Certificates:Certificate1:Source"] = "Store",
["TestConfig:Certificates:Certificate1:Subject"] = "localhost",
["TestConfig:Certificates:Certificate1:StoreName"] = "My",
["TestConfig:Certificates:Certificate1:StoreLocation"] = "CurrentUser",
})
.Build();
var loggerFactory = new Mock<ILoggerFactory>();
var logger = new MockLogger();
loggerFactory
.Setup(factory => factory.CreateLogger("Microsoft.AspNetCore.CertificateLoader"))
.Returns(logger);
var certificateLoader = new CertificateLoader(
null,
loggerFactory.Object,
null,
Mock.Of<ICertificateFileLoader>(),
Mock.Of<ICertificateStoreLoader>());
var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificates"));
Assert.Equal(0, loadedCertificates.Count());
Assert.Single(logger.LogMessages, logMessage =>
logMessage.LogLevel == LogLevel.Warning &&
logMessage.Message == "Unable to find a matching certificate for subject 'localhost' in store 'My' in 'CurrentUser'.");
}
private class MockLogger : ILogger
{
private readonly List<LogMessage> _logMessages = new List<LogMessage>();
public IEnumerable<LogMessage> LogMessages => _logMessages;
public IDisposable BeginScope<TState>(TState state)
{
throw new NotImplementedException();
}
public bool IsEnabled(LogLevel logLevel)
{
throw new NotImplementedException();
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
_logMessages.Add(new LogMessage
{
LogLevel = logLevel,
Message = formatter(state, exception)
});
}
public class LogMessage
{
public LogLevel LogLevel { get; set; }
public string Message { get; set; }
}
}
} }
} }