diff --git a/samples/AppSettings/Program.cs b/samples/AppSettings/Program.cs index 5db5710..13fbeeb 100644 --- a/samples/AppSettings/Program.cs +++ b/samples/AppSettings/Program.cs @@ -13,7 +13,7 @@ public static void Main(string[] args) { 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(); } } diff --git a/src/Microsoft.AspNetCore/CertificateLoader.cs b/src/Microsoft.AspNetCore/CertificateLoader.cs index be006b4..59115b0 100644 --- a/src/Microsoft.AspNetCore/CertificateLoader.cs +++ b/src/Microsoft.AspNetCore/CertificateLoader.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore { @@ -15,14 +16,17 @@ namespace Microsoft.AspNetCore public class CertificateLoader { private readonly IConfiguration _certificatesConfiguration; + private readonly string _environmentName; private readonly ICertificateFileLoader _certificateFileLoader; private readonly ICertificateStoreLoader _certificateStoreLoader; + private readonly ILogger _logger; /// - /// Creates a new instance of . + /// Creates a new instance of that can load certificate references from configuration. /// - public CertificateLoader() - : this(null) + /// An with information about certificates. + public CertificateLoader(IConfiguration certificatesConfiguration) + : this(certificatesConfiguration, null, null) { } @@ -30,17 +34,35 @@ public CertificateLoader() /// Creates a new instance of that can load certificate references from configuration. /// /// An with information about certificates. - public CertificateLoader(IConfiguration certificatesConfiguration) - : this(certificatesConfiguration, new CertificateFileLoader(), new CertificateStoreLoader()) + /// An instance. + public CertificateLoader(IConfiguration certificatesConfiguration, ILoggerFactory loggerFactory) + : this(certificatesConfiguration, loggerFactory, null) { - _certificatesConfiguration = certificatesConfiguration; } - internal CertificateLoader(IConfiguration certificatesConfiguration, ICertificateFileLoader certificateFileLoader, ICertificateStoreLoader certificateStoreLoader) + /// + /// Creates a new instance of that can load certificate references from configuration. + /// + /// An with information about certificates. + /// An instance. + /// The name of the environment the application is running in. + 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; _certificateFileLoader = certificateFileLoader; _certificateStoreLoader = certificateStoreLoader; + _logger = loggerFactory?.CreateLogger("Microsoft.AspNetCore.CertificateLoader"); } /// @@ -99,7 +121,8 @@ private X509Certificate2 LoadSingle(string certificateName) 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); @@ -116,10 +139,10 @@ private X509Certificate2 LoadSingle(IConfigurationSection certificateConfigurati certificateSource = new CertificateFileSource(_certificateFileLoader); break; case "store": - certificateSource = new CertificateStoreSource(_certificateStoreLoader); + certificateSource = new CertificateStoreSource(_certificateStoreLoader, _logger); break; default: - throw new InvalidOperationException($"Invalid certificate source kind: {sourceKind}"); + throw new InvalidOperationException($"Invalid certificate source kind '{sourceKind}'."); } certificateConfiguration.Bind(certificateSource); @@ -163,7 +186,7 @@ public override X509Certificate2 Load() if (error != null) { - throw error; + throw new InvalidOperationException($"Unable to load certificate from file '{Path}'. Error details: '{error.Message}'.", error); } return certificate; @@ -188,10 +211,12 @@ private X509Certificate2 TryLoad(X509KeyStorageFlags flags, out Exception except private class CertificateStoreSource : CertificateSource { private readonly ICertificateStoreLoader _certificateStoreLoader; + private readonly ILogger _logger; - public CertificateStoreSource(ICertificateStoreLoader certificateStoreLoader) + public CertificateStoreSource(ICertificateStoreLoader certificateStoreLoader, ILogger logger) { _certificateStoreLoader = certificateStoreLoader; + _logger = logger; } public string Subject { get; set; } @@ -203,10 +228,17 @@ public override X509Certificate2 Load() { 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."); + } + + 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 _certificateStoreLoader.Load(Subject, StoreName, storeLocation, !AllowInvalid); + return certificate; } } } diff --git a/src/Microsoft.AspNetCore/KestrelServerOptionsSetup.cs b/src/Microsoft.AspNetCore/KestrelServerOptionsSetup.cs index d2ec6f8..a76e7f6 100644 --- a/src/Microsoft.AspNetCore/KestrelServerOptionsSetup.cs +++ b/src/Microsoft.AspNetCore/KestrelServerOptionsSetup.cs @@ -4,20 +4,29 @@ using System; using System.Linq; using System.Net; +using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore { internal class KestrelServerOptionsSetup : IConfigureOptions { + private readonly IHostingEnvironment _hostingEnvironment; private readonly IConfiguration _configurationRoot; + private readonly ILoggerFactory _loggerFactory; - public KestrelServerOptionsSetup(IConfiguration configurationRoot) + public KestrelServerOptionsSetup( + IHostingEnvironment hostingEnvironment, + IConfiguration configurationRoot, + ILoggerFactory loggerFactory) { + _hostingEnvironment = hostingEnvironment; _configurationRoot = configurationRoot; + _loggerFactory = loggerFactory; } public void Configure(KestrelServerOptions options) @@ -27,7 +36,7 @@ public void Configure(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()) { @@ -56,14 +65,22 @@ private void BindEndPoint( options.Listen(address, port, listenOptions => { var certificateConfig = endPoint.GetSection("Certificate"); + X509Certificate2 certificate; if (certificateConfig.Exists()) { - var certificate = certificateLoader.Load(certificateConfig).FirstOrDefault(); + try + { + certificate = certificateLoader.Load(certificateConfig).FirstOrDefault(); - if (certificate == null) + if (certificate == null) + { + throw new InvalidOperationException($"No certificate found for endpoint '{endPoint.Key}'."); + } + } + catch (Exception ex) { - throw new InvalidOperationException($"Unable to load certificate for endpoint '{endPoint.Key}'"); + 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); diff --git a/test/Microsoft.AspNetCore.FunctionalTests/CertificateLoaderTests.cs b/test/Microsoft.AspNetCore.FunctionalTests/CertificateLoaderTests.cs index 7cf3787..1db8765 100644 --- a/test/Microsoft.AspNetCore.FunctionalTests/CertificateLoaderTests.cs +++ b/test/Microsoft.AspNetCore.FunctionalTests/CertificateLoaderTests.cs @@ -6,8 +6,9 @@ using System.Linq; using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.Configuration; -using Xunit; +using Microsoft.Extensions.Logging; using Moq; +using Xunit; namespace Microsoft.AspNetCore.FunctionalTests { @@ -35,6 +36,8 @@ public void Loads_SingleCertificateName_File() var certificateLoader = new CertificateLoader( configuration.GetSection("Certificates"), + null, + null, certificateFileLoader.Object, Mock.Of()); @@ -45,31 +48,28 @@ public void Loads_SingleCertificateName_File() } [Fact] - public void Throws_SingleCertificateName_File_KeyNotFound() + public void Throws_SingleCertificateName_KeyNotFound() { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["Certificates:Certificate1:Source"] = "File", - ["Certificates:Certificate1:Path"] = "Certificate1.pfx", - ["Certificates:Certificate1:Password"] = "Password1", - ["TestConfig:Certificate"] = "Certificate2" + ["TestConfig:Certificate"] = "Certificate1" }) .Build(); - var certificate = new X509Certificate2(); - var certificateLoader = new CertificateLoader( configuration.GetSection("Certificates"), + null, + null, Mock.Of(), Mock.Of()); - var exception = Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"))); - Assert.Equal("No certificate named Certificate2 found in configuration", exception.Message); + var exception = Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"))); + Assert.Equal("No certificate named 'Certificate1' found in configuration for the current environment.", exception.Message); } [Fact] - public void Throws_SingleCertificateName_File_FileNotFound() + public void Throws_SingleCertificateName_File_FileLoadError() { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -81,19 +81,20 @@ public void Throws_SingleCertificateName_File_FileNotFound() }) .Build(); - var exception = new Exception(); - var certificateFileLoader = new Mock(); certificateFileLoader .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny())) - .Callback(() => throw exception); + .Callback(() => throw new Exception(nameof(Throws_SingleCertificateName_File_FileLoadError))); var certificateLoader = new CertificateLoader( configuration.GetSection("Certificates"), + null, + null, certificateFileLoader.Object, Mock.Of()); - Assert.Same(exception, Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")))); + var exception = Assert.Throws(() => 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] @@ -119,6 +120,8 @@ public void Loads_SingleCertificateName_Store() var certificateLoader = new CertificateLoader( configuration.GetSection("Certificates"), + null, + null, Mock.Of(), certificateStoreLoader.Object); @@ -127,30 +130,7 @@ public void Loads_SingleCertificateName_Store() Assert.Same(certificate, loadedCertificates.ElementAt(0)); certificateStoreLoader.VerifyAll(); } - - [Fact] - public void Throws_SingleCertificateName_Store_KeyNotFound() - { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - ["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(), - Mock.Of()); - - var exception = Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"))); - Assert.Equal("No certificate named Certificate2 found in configuration", exception.Message); - } - + [Fact] public void ReturnsNull_SingleCertificateName_Store_NotFoundInStore() { @@ -172,6 +152,8 @@ public void ReturnsNull_SingleCertificateName_Store_NotFoundInStore() var certificateLoader = new CertificateLoader( configuration.GetSection("Certificates"), + null, + null, Mock.Of(), certificateStoreLoader.Object); @@ -209,6 +191,8 @@ public void Loads_MultipleCertificateNames_File() var certificateLoader = new CertificateLoader( configuration.GetSection("Certificates"), + null, + null, certificateFileLoader.Object, Mock.Of()); @@ -219,51 +203,10 @@ public void Loads_MultipleCertificateNames_File() certificateFileLoader.VerifyAll(); } - [Theory] - [InlineData("Certificate1;NotFound")] - [InlineData("Certificate1;Certificate2;NotFound")] - [InlineData("NotFound;Certificate1")] - [InlineData("NotFound;Certificate1;Certificate2")] - [InlineData("Certificate1;NotFound;Certificate2")] - public void Throws_MultipleCertificateNames_File_KeyNotFound(string certificateNames) - { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - ["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 certificate2 = new X509Certificate2(); - - var certificateFileLoader = new Mock(); - certificateFileLoader - .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny())) - .Returns(certificate1); - certificateFileLoader - .Setup(loader => loader.Load("Certificate2.pfx", "Password2", It.IsAny())) - .Returns(certificate2); - - var certificateLoader = new CertificateLoader( - configuration.GetSection("Certificates"), - certificateFileLoader.Object, - Mock.Of()); - - var exception = Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"))); - Assert.Equal("No certificate named NotFound found in configuration", exception.Message); - } - [Theory] [InlineData("Certificate1;Certificate2")] [InlineData("Certificate2;Certificate1")] - public void Throws_MultipleCertificateNames_File_FileNotFound(string certificateNames) + public void Throws_MultipleCertificateNames_File_FileLoadError(string certificateNames) { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -279,7 +222,6 @@ public void Throws_MultipleCertificateNames_File_FileNotFound(string certificate .Build(); var certificate1 = new X509Certificate2(); - var exception = new Exception(); var certificateFileLoader = new Mock(); certificateFileLoader @@ -287,14 +229,17 @@ public void Throws_MultipleCertificateNames_File_FileNotFound(string certificate .Returns(certificate1); certificateFileLoader .Setup(loader => loader.Load("Certificate2.pfx", "Password2", It.IsAny())) - .Throws(exception); + .Throws(new Exception(nameof(Throws_MultipleCertificateNames_File_FileLoadError))); var certificateLoader = new CertificateLoader( configuration.GetSection("Certificates"), + null, + null, certificateFileLoader.Object, Mock.Of()); - Assert.Same(exception, Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")))); + var exception = Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"))); + Assert.Equal($"Unable to load certificate from file 'Certificate2.pfx'. Error details: '{nameof(Throws_MultipleCertificateNames_File_FileLoadError)}'.", exception.Message); } [Fact] @@ -328,6 +273,8 @@ public void Loads_MultipleCertificateNames_Store() var certificateLoader = new CertificateLoader( configuration.GetSection("Certificates"), + null, + null, Mock.Of(), certificateStoreLoader.Object); @@ -338,49 +285,6 @@ public void Loads_MultipleCertificateNames_Store() 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 - { - ["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(); - certificateStoreLoader - .Setup(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny())) - .Returns(certificate1); - certificateStoreLoader - .Setup(loader => loader.Load("example.com", "Root", StoreLocation.LocalMachine, It.IsAny())) - .Returns(certificate2); - - var certificateLoader = new CertificateLoader( - configuration.GetSection("Certificates"), - Mock.Of(), - certificateStoreLoader.Object); - - var exception = Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"))); - Assert.Equal("No certificate named NotFound found in configuration", exception.Message); - } - [Theory] [InlineData("Certificate1;Certificate2", 1)] [InlineData("Certificate2;Certificate1", 1)] @@ -421,6 +325,8 @@ public void ReturnsNull_MultipleCertificateNames_Store_NotFoundInStore(string ce var certificateLoader = new CertificateLoader( configuration.GetSection("Certificates"), + null, + null, Mock.Of(), certificateStoreLoader.Object); @@ -460,6 +366,8 @@ public void Loads_MultipleCertificateNames_FileAndStore() var certificateLoader = new CertificateLoader( configuration.GetSection("Certificates"), + null, + null, certificateFileLoader.Object, certificateStoreLoader.Object); @@ -475,7 +383,7 @@ public void Loads_MultipleCertificateNames_FileAndStore() [InlineData("Certificate1;Certificate2;NotFound")] [InlineData("Certificate1;NotFound;Certificate2")] [InlineData("NotFound;Certificate1;Certificate2")] - public void Throws_MultipleCertificateNames_FileAndStore_KeyNotFound(string certificateNames) + public void Throws_MultipleCertificateNames_KeyNotFound(string certificateNames) { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -506,17 +414,19 @@ public void Throws_MultipleCertificateNames_FileAndStore_KeyNotFound(string cert var certificateLoader = new CertificateLoader( configuration.GetSection("Certificates"), + null, + null, certificateFileLoader.Object, certificateStoreLoader.Object); - var exception = Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"))); - Assert.Equal("No certificate named NotFound found in configuration", exception.Message); + var exception = Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"))); + Assert.Equal("No certificate named 'NotFound' found in configuration for the current environment.", exception.Message); } [Theory] [InlineData("Certificate1;Certificate2")] [InlineData("Certificate2;Certificate1")] - public void Throws_MultipleCertificateNames_FileAndStore_FileNotFound(string certificateNames) + public void Throws_MultipleCertificateNames_FileAndStore_FileLoadError(string certificateNames) { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -532,13 +442,12 @@ public void Throws_MultipleCertificateNames_FileAndStore_FileNotFound(string cer }) .Build(); - var exception = new Exception(); var storeCertificate = new X509Certificate2(); var certificateFileLoader = new Mock(); certificateFileLoader .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny())) - .Throws(exception); + .Throws(new Exception(nameof(Throws_MultipleCertificateNames_FileAndStore_FileLoadError))); var certificateStoreLoader = new Mock(); certificateStoreLoader @@ -547,10 +456,13 @@ public void Throws_MultipleCertificateNames_FileAndStore_FileNotFound(string cer var certificateLoader = new CertificateLoader( configuration.GetSection("Certificates"), + null, + null, certificateFileLoader.Object, certificateStoreLoader.Object); - Assert.Same(exception, Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")))); + var exception = Assert.Throws(() => 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] @@ -586,6 +498,8 @@ public void ReturnsNull_MultipleCertificateNames_FileAndStore_NotFoundInStore(st var certificateLoader = new CertificateLoader( configuration.GetSection("Certificates"), + null, + null, certificateFileLoader.Object, certificateStoreLoader.Object); @@ -616,6 +530,8 @@ public void Loads_SingleCertificateInline_File() .Returns(certificate); var certificateLoader = new CertificateLoader( + null, + null, null, certificateFileLoader.Object, Mock.Of()); @@ -627,7 +543,7 @@ public void Loads_SingleCertificateInline_File() } [Fact] - public void Throws_SingleCertificateInline_FileNotFound() + public void Throws_SingleCertificateInline_FileLoadError() { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -638,19 +554,20 @@ public void Throws_SingleCertificateInline_FileNotFound() }) .Build(); - var exception = new Exception(); - var certificateFileLoader = new Mock(); certificateFileLoader .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny())) - .Throws(exception); + .Throws(new Exception(nameof(Throws_SingleCertificateInline_FileLoadError))); var certificateLoader = new CertificateLoader( + null, + null, null, certificateFileLoader.Object, Mock.Of()); - Assert.Same(exception, Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")))); + var exception = Assert.Throws(() => 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(); } @@ -675,6 +592,8 @@ public void Loads_SingleCertificateInline_Store() .Returns(certificate); var certificateLoader = new CertificateLoader( + null, + null, null, Mock.Of(), certificateStoreLoader.Object); @@ -701,6 +620,8 @@ public void ReturnsNull_SingleCertificateInline_Store_NotFoundInStore() var certificateStoreLoader = new Mock(); var certificateLoader = new CertificateLoader( + null, + null, null, Mock.Of(), certificateStoreLoader.Object); @@ -737,6 +658,8 @@ public void Loads_MultipleCertificatesInline_File() .Returns(certificate2); var certificateLoader = new CertificateLoader( + null, + null, null, certificateFileLoader.Object, Mock.Of()); @@ -749,7 +672,7 @@ public void Loads_MultipleCertificatesInline_File() } [Fact] - public void Throws_MultipleCertificatesInline_File_FileNotFound() + public void Throws_MultipleCertificatesInline_File_FileLoadError() { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -764,7 +687,6 @@ public void Throws_MultipleCertificatesInline_File_FileNotFound() .Build(); var certificate1 = new X509Certificate2(); - var exception = new Exception(); var certificateFileLoader = new Mock(); certificateFileLoader @@ -772,14 +694,17 @@ public void Throws_MultipleCertificatesInline_File_FileNotFound() .Returns(certificate1); certificateFileLoader .Setup(loader => loader.Load("Certificate2.pfx", "Password2", It.IsAny())) - .Throws(exception); + .Throws(new Exception(nameof(Throws_MultipleCertificatesInline_File_FileLoadError))); var certificateLoader = new CertificateLoader( + null, + null, null, certificateFileLoader.Object, Mock.Of()); - Assert.Same(exception, Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificates")))); + var exception = Assert.Throws(() => 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] @@ -811,6 +736,8 @@ public void Loads_MultipleCertificatesInline_Store() .Returns(certificate2); var certificateLoader = new CertificateLoader( + null, + null, null, Mock.Of(), certificateStoreLoader.Object); @@ -851,6 +778,8 @@ public void ReturnsNull_MultipleCertificatesInline_Store_NotFoundInStore() .Returns(certificate); var certificateLoader = new CertificateLoader( + null, + null, null, Mock.Of(), certificateStoreLoader.Object); @@ -908,6 +837,8 @@ public void Loads_MultipleCertificatesInline_FileAndStore() .Returns(storeCertificate2); var certificateLoader = new CertificateLoader( + null, + null, null, certificateFileLoader.Object, certificateStoreLoader.Object); @@ -922,7 +853,7 @@ public void Loads_MultipleCertificatesInline_FileAndStore() } [Fact] - public void Throws_MultipleCertificatesInline_FileAndStore_FileNotFound() + public void Throws_MultipleCertificatesInline_FileAndStore_FileLoadError() { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -937,13 +868,12 @@ public void Throws_MultipleCertificatesInline_FileAndStore_FileNotFound() }) .Build(); - var exception = new Exception(); var certificate = new X509Certificate2(); var certificateFileLoader = new Mock(); certificateFileLoader .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny())) - .Throws(exception); + .Throws(new Exception(nameof(Throws_MultipleCertificatesInline_FileAndStore_FileLoadError))); var certificateStoreLoader = new Mock(); certificateStoreLoader @@ -951,11 +881,14 @@ public void Throws_MultipleCertificatesInline_FileAndStore_FileNotFound() .Returns(certificate); var certificateLoader = new CertificateLoader( + null, + null, null, certificateFileLoader.Object, certificateStoreLoader.Object); - Assert.Same(exception, Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificates")))); + var exception = Assert.Throws(() => 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] @@ -987,6 +920,8 @@ public void ReturnsNull_MultipleCertificatesInline_FileAndStore_NotFoundInStore( .Returns(null); var certificateLoader = new CertificateLoader( + null, + null, null, certificateFileLoader.Object, certificateStoreLoader.Object); @@ -995,5 +930,115 @@ public void ReturnsNull_MultipleCertificatesInline_FileAndStore_NotFoundInStore( Assert.Equal(1, loadedCertificates.Count()); Assert.Same(certificate, loadedCertificates.ElementAt(0)); } + + [Theory] + [InlineData("Development")] + [InlineData("Production")] + public void IncludesEnvironmentNameInExceptionWhenAvailable(string environmentName) + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["TestConfig:Certificate"] = "Certificate1" + }) + .Build(); + + var certificateLoader = new CertificateLoader( + configuration.GetSection("Certificates"), + null, + environmentName, + Mock.Of(), + Mock.Of()); + + var exception = Assert.Throws(() => 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 + { + ["TestConfig:Certificate"] = "Certificate1" + }) + .Build(); + + var certificateLoader = new CertificateLoader( + configuration.GetSection("Certificates"), + null, + null, + Mock.Of(), + Mock.Of()); + + var exception = Assert.Throws(() => 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 + { + ["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(); + var logger = new MockLogger(); + + loggerFactory + .Setup(factory => factory.CreateLogger("Microsoft.AspNetCore.CertificateLoader")) + .Returns(logger); + + var certificateLoader = new CertificateLoader( + null, + loggerFactory.Object, + null, + Mock.Of(), + Mock.Of()); + + 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 _logMessages = new List(); + + public IEnumerable LogMessages => _logMessages; + + public IDisposable BeginScope(TState state) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) + { + throw new NotImplementedException(); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + _logMessages.Add(new LogMessage + { + LogLevel = logLevel, + Message = formatter(state, exception) + }); + } + + public class LogMessage + { + public LogLevel LogLevel { get; set; } + public string Message { get; set; } + } + } } }