diff --git a/Hosting.sln b/Hosting.sln
index 8aafc60dd3..f61f83d717 100644
--- a/Hosting.sln
+++ b/Hosting.sln
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
-VisualStudioVersion = 15.0.26510.0
+VisualStudioVersion = 15.0.26524.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E0497F39-AFFB-4819-A116-E39E361915AB}"
EndProject
@@ -34,6 +34,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Hostin
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Hosting.WindowsServices", "src\Microsoft.AspNetCore.Hosting.WindowsServices\Microsoft.AspNetCore.Hosting.WindowsServices.csproj", "{9C93A93B-270A-4785-8F41-46C38DC33825}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Certificates.Configuration", "src\Microsoft.AspNetCore.Certificates.Configuration\Microsoft.AspNetCore.Certificates.Configuration.csproj", "{A911A891-EAC5-4AA1-B17E-B83A1BB1E8B4}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Certificates.Configuration.Tests", "test\Microsoft.AspNetCore.Certificates.Configuration.Tests\Microsoft.AspNetCore.Certificates.Configuration.Tests.csproj", "{AB0B7394-278D-4838-A59C-276ED88D00CC}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -192,6 +196,30 @@ Global
{9C93A93B-270A-4785-8F41-46C38DC33825}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{9C93A93B-270A-4785-8F41-46C38DC33825}.Release|x86.ActiveCfg = Release|Any CPU
{9C93A93B-270A-4785-8F41-46C38DC33825}.Release|x86.Build.0 = Release|Any CPU
+ {A911A891-EAC5-4AA1-B17E-B83A1BB1E8B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A911A891-EAC5-4AA1-B17E-B83A1BB1E8B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A911A891-EAC5-4AA1-B17E-B83A1BB1E8B4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {A911A891-EAC5-4AA1-B17E-B83A1BB1E8B4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {A911A891-EAC5-4AA1-B17E-B83A1BB1E8B4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A911A891-EAC5-4AA1-B17E-B83A1BB1E8B4}.Debug|x86.Build.0 = Debug|Any CPU
+ {A911A891-EAC5-4AA1-B17E-B83A1BB1E8B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A911A891-EAC5-4AA1-B17E-B83A1BB1E8B4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A911A891-EAC5-4AA1-B17E-B83A1BB1E8B4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {A911A891-EAC5-4AA1-B17E-B83A1BB1E8B4}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {A911A891-EAC5-4AA1-B17E-B83A1BB1E8B4}.Release|x86.ActiveCfg = Release|Any CPU
+ {A911A891-EAC5-4AA1-B17E-B83A1BB1E8B4}.Release|x86.Build.0 = Release|Any CPU
+ {AB0B7394-278D-4838-A59C-276ED88D00CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AB0B7394-278D-4838-A59C-276ED88D00CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AB0B7394-278D-4838-A59C-276ED88D00CC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {AB0B7394-278D-4838-A59C-276ED88D00CC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {AB0B7394-278D-4838-A59C-276ED88D00CC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AB0B7394-278D-4838-A59C-276ED88D00CC}.Debug|x86.Build.0 = Debug|Any CPU
+ {AB0B7394-278D-4838-A59C-276ED88D00CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AB0B7394-278D-4838-A59C-276ED88D00CC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AB0B7394-278D-4838-A59C-276ED88D00CC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {AB0B7394-278D-4838-A59C-276ED88D00CC}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {AB0B7394-278D-4838-A59C-276ED88D00CC}.Release|x86.ActiveCfg = Release|Any CPU
+ {AB0B7394-278D-4838-A59C-276ED88D00CC}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -210,5 +238,7 @@ Global
{39D3B138-37DB-4D03-A5A0-3F2B02EFC671} = {FEB39027-9158-4DE2-997F-7ADAEF8188D0}
{96BC7EEA-64D9-4DA5-8E87-1C18CBFE7D12} = {E0497F39-AFFB-4819-A116-E39E361915AB}
{9C93A93B-270A-4785-8F41-46C38DC33825} = {E0497F39-AFFB-4819-A116-E39E361915AB}
+ {A911A891-EAC5-4AA1-B17E-B83A1BB1E8B4} = {E0497F39-AFFB-4819-A116-E39E361915AB}
+ {AB0B7394-278D-4838-A59C-276ED88D00CC} = {FEB39027-9158-4DE2-997F-7ADAEF8188D0}
EndGlobalSection
EndGlobal
diff --git a/src/Microsoft.AspNetCore.Certificates.Configuration/CertificateFileLoader.cs b/src/Microsoft.AspNetCore.Certificates.Configuration/CertificateFileLoader.cs
new file mode 100644
index 0000000000..bcaa93d3e3
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Certificates.Configuration/CertificateFileLoader.cs
@@ -0,0 +1,15 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Security.Cryptography.X509Certificates;
+
+namespace Microsoft.AspNetCore.Certificates.Configuration
+{
+ internal class CertificateFileLoader : ICertificateFileLoader
+ {
+ public X509Certificate2 Load(string path, string password, X509KeyStorageFlags flags)
+ {
+ return new X509Certificate2(path, password, flags);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Certificates.Configuration/CertificateLoader.cs b/src/Microsoft.AspNetCore.Certificates.Configuration/CertificateLoader.cs
new file mode 100644
index 0000000000..03c2f8885f
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Certificates.Configuration/CertificateLoader.cs
@@ -0,0 +1,248 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Certificates.Configuration
+{
+ ///
+ /// A helper class to load certificates from files and certificate stores based on data.
+ ///
+ 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 that can load certificate references from configuration.
+ ///
+ /// An with information about certificates.
+ public CertificateLoader(IConfiguration certificatesConfiguration)
+ : this(certificatesConfiguration, null, null)
+ {
+ }
+
+ ///
+ /// Creates a new instance of that can load certificate references from configuration.
+ ///
+ /// An with information about certificates.
+ /// An instance.
+ public CertificateLoader(IConfiguration certificatesConfiguration, ILoggerFactory loggerFactory)
+ : this(certificatesConfiguration, loggerFactory, null)
+ {
+ }
+
+ ///
+ /// 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");
+ }
+
+ ///
+ /// Loads one or more certificates based on the information found in a configuration section.
+ ///
+ /// A configuration section containing either a string value referencing certificates
+ /// by name, or one or more inline certificate specifications.
+ ///
+ /// One or more loaded certificates.
+ public IEnumerable Load(IConfigurationSection certificateConfiguration)
+ {
+ var certificateNames = certificateConfiguration.Value;
+ var certificates = new List();
+
+ if (certificateNames != null)
+ {
+ foreach (var certificateName in certificateNames.Split(';'))
+ {
+ var certificate = LoadSingle(certificateName);
+ if (certificate != null)
+ {
+ certificates.Add(certificate);
+ }
+ }
+ }
+ else
+ {
+ if (certificateConfiguration["Source"] != null)
+ {
+ var certificate = LoadSingle(certificateConfiguration);
+ if (certificate != null)
+ {
+ certificates.Add(certificate);
+ }
+ }
+ else
+ {
+ certificates.AddRange(LoadMultiple(certificateConfiguration));
+ }
+ }
+
+ return certificates;
+ }
+
+ ///
+ /// Loads a certificate by name.
+ ///
+ /// The certificate name.
+ /// The loaded certificate
+ /// This method only works if the instance was constructed with
+ /// a reference to an instance containing named certificates.
+ ///
+ private X509Certificate2 LoadSingle(string certificateName)
+ {
+ var certificateConfiguration = _certificatesConfiguration?.GetSection(certificateName);
+
+ if (!certificateConfiguration.Exists())
+ {
+ var environmentName = _environmentName != null ? $" ({_environmentName})" : "";
+ throw new KeyNotFoundException($"No certificate named '{certificateName}' found in configuration for the current environment{environmentName}.");
+ }
+
+ return LoadSingle(certificateConfiguration);
+ }
+
+ private X509Certificate2 LoadSingle(IConfigurationSection certificateConfiguration)
+ {
+ var sourceKind = certificateConfiguration["Source"];
+
+ CertificateSource certificateSource;
+ switch (sourceKind.ToLowerInvariant())
+ {
+ case "file":
+ certificateSource = new CertificateFileSource(_certificateFileLoader);
+ break;
+ case "store":
+ certificateSource = new CertificateStoreSource(_certificateStoreLoader, _logger);
+ break;
+ default:
+ throw new InvalidOperationException($"Invalid certificate source kind '{sourceKind}'.");
+ }
+
+ certificateConfiguration.Bind(certificateSource);
+
+ return certificateSource.Load();
+ }
+
+ private IEnumerable LoadMultiple(IConfigurationSection certificatesConfiguration)
+ => certificatesConfiguration.GetChildren()
+ .Select(LoadSingle)
+ .Where(c => c != null);
+
+ private abstract class CertificateSource
+ {
+ public string Source { get; set; }
+
+ public abstract X509Certificate2 Load();
+ }
+
+ private class CertificateFileSource : CertificateSource
+ {
+ private ICertificateFileLoader _certificateFileLoader;
+
+ public CertificateFileSource(ICertificateFileLoader certificateFileLoader)
+ {
+ _certificateFileLoader = certificateFileLoader;
+ }
+
+ public string Path { get; set; }
+
+ public string Password { get; set; }
+
+ public override X509Certificate2 Load()
+ {
+ var certificate = TryLoad(X509KeyStorageFlags.DefaultKeySet, out var error)
+ ?? TryLoad(X509KeyStorageFlags.UserKeySet, out error)
+#if NETCOREAPP2_0
+ ?? TryLoad(X509KeyStorageFlags.EphemeralKeySet, out error)
+#elif NETSTANDARD2_0
+#else
+#error target frameworks need to be updated
+#endif
+ ;
+
+ if (error != null)
+ {
+ throw new InvalidOperationException($"Unable to load certificate from file '{Path}'. Error details: '{error.Message}'.", error);
+ }
+
+ return certificate;
+ }
+
+ private X509Certificate2 TryLoad(X509KeyStorageFlags flags, out Exception exception)
+ {
+ try
+ {
+ var loadedCertificate = _certificateFileLoader.Load(Path, Password, flags);
+ exception = null;
+ return loadedCertificate;
+ }
+ catch (Exception e)
+ {
+ exception = e;
+ return null;
+ }
+ }
+ }
+
+ private class CertificateStoreSource : CertificateSource
+ {
+ private readonly ICertificateStoreLoader _certificateStoreLoader;
+ private readonly ILogger _logger;
+
+ public CertificateStoreSource(ICertificateStoreLoader certificateStoreLoader, ILogger logger)
+ {
+ _certificateStoreLoader = certificateStoreLoader;
+ _logger = logger;
+ }
+
+ public string Subject { get; set; }
+ public string StoreName { get; set; }
+ public string StoreLocation { get; set; }
+ public bool AllowInvalid { get; set; }
+
+ public override X509Certificate2 Load()
+ {
+ if (!Enum.TryParse(StoreLocation, ignoreCase: true, result: out StoreLocation 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 certificate;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Certificates.Configuration/CertificateStoreLoader.cs b/src/Microsoft.AspNetCore.Certificates.Configuration/CertificateStoreLoader.cs
new file mode 100644
index 0000000000..1329fd4475
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Certificates.Configuration/CertificateStoreLoader.cs
@@ -0,0 +1,67 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Linq;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Microsoft.AspNetCore.Certificates.Configuration
+{
+ ///
+ /// Loads certificates from certificate stores.
+ ///
+ public class CertificateStoreLoader : ICertificateStoreLoader
+ {
+ ///
+ /// Load a ceritificate from the given store location.
+ ///
+ /// The certificate subject name to match.
+ /// The store to open.
+ /// The store location to open.
+ /// Only load currently valid certs.
+ /// The closest matching vertificate.
+ public X509Certificate2 Load(string subject, string storeName, StoreLocation storeLocation, bool validOnly)
+ {
+ using (var store = new X509Store(storeName, storeLocation))
+ {
+ X509Certificate2Collection storeCertificates = null;
+ X509Certificate2Collection foundCertificates = null;
+ X509Certificate2 foundCertificate = null;
+
+ try
+ {
+ store.Open(OpenFlags.ReadOnly);
+ storeCertificates = store.Certificates;
+ foundCertificates = storeCertificates.Find(X509FindType.FindBySubjectDistinguishedName, subject, validOnly);
+ foundCertificate = foundCertificates
+ .OfType()
+ .OrderByDescending(certificate => certificate.NotAfter)
+ .FirstOrDefault();
+
+ return foundCertificate;
+ }
+ finally
+ {
+ if (foundCertificate != null)
+ {
+ storeCertificates.Remove(foundCertificate);
+ foundCertificates.Remove(foundCertificate);
+ }
+
+ DisposeCertificates(storeCertificates);
+ DisposeCertificates(foundCertificates);
+ }
+ }
+ }
+
+ private void DisposeCertificates(X509Certificate2Collection certificates)
+ {
+ if (certificates != null)
+ {
+ foreach (var certificate in certificates)
+ {
+ certificate.Dispose();
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Certificates.Configuration/ICertificateFileLoader.cs b/src/Microsoft.AspNetCore.Certificates.Configuration/ICertificateFileLoader.cs
new file mode 100644
index 0000000000..2a820eea39
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Certificates.Configuration/ICertificateFileLoader.cs
@@ -0,0 +1,12 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Security.Cryptography.X509Certificates;
+
+namespace Microsoft.AspNetCore.Certificates.Configuration
+{
+ internal interface ICertificateFileLoader
+ {
+ X509Certificate2 Load(string path, string password, X509KeyStorageFlags flags);
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Certificates.Configuration/ICertificateStoreLoader.cs b/src/Microsoft.AspNetCore.Certificates.Configuration/ICertificateStoreLoader.cs
new file mode 100644
index 0000000000..2a7c334759
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Certificates.Configuration/ICertificateStoreLoader.cs
@@ -0,0 +1,12 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Security.Cryptography.X509Certificates;
+
+namespace Microsoft.AspNetCore.Certificates.Configuration
+{
+ internal interface ICertificateStoreLoader
+ {
+ X509Certificate2 Load(string subject, string storeName, StoreLocation storeLocation, bool validOnly);
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Certificates.Configuration/Microsoft.AspNetCore.Certificates.Configuration.csproj b/src/Microsoft.AspNetCore.Certificates.Configuration/Microsoft.AspNetCore.Certificates.Configuration.csproj
new file mode 100644
index 0000000000..5275d54e04
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Certificates.Configuration/Microsoft.AspNetCore.Certificates.Configuration.csproj
@@ -0,0 +1,17 @@
+
+
+
+
+
+ netcoreapp2.0;netstandard2.0
+ aspnetcore;certificates
+ Helpers for loading certificates from configuration.
+ true
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.AspNetCore.Certificates.Configuration/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Certificates.Configuration/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..61148132fc
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Certificates.Configuration/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Certificates.Configuration.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Certificates.Configuration.Tests/CertificateLoaderTests.cs b/test/Microsoft.AspNetCore.Certificates.Configuration.Tests/CertificateLoaderTests.cs
new file mode 100644
index 0000000000..b3550bf3d8
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Certificates.Configuration.Tests/CertificateLoaderTests.cs
@@ -0,0 +1,1044 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Certificates.Configuration.Tests
+{
+ public class CertificateLoaderTests
+ {
+ [Fact]
+ public void Loads_SingleCertificateName_File()
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["Certificates:Certificate1:Source"] = "File",
+ ["Certificates:Certificate1:Path"] = "Certificate1.pfx",
+ ["Certificates:Certificate1:Password"] = "Password1",
+ ["TestConfig:Certificate"] = "Certificate1"
+ })
+ .Build();
+
+ var certificate = new X509Certificate2();
+
+ var certificateFileLoader = new Mock();
+ certificateFileLoader
+ .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny()))
+ .Returns(certificate);
+
+ var certificateLoader = new CertificateLoader(
+ configuration.GetSection("Certificates"),
+ null,
+ null,
+ certificateFileLoader.Object,
+ Mock.Of());
+
+ var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"));
+ Assert.Equal(1, loadedCertificates.Count());
+ Assert.Same(certificate, loadedCertificates.ElementAt(0));
+ certificateFileLoader.VerifyAll();
+ }
+
+ [Fact]
+ public void Throws_SingleCertificateName_KeyNotFound()
+ {
+ 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 Throws_SingleCertificateName_File_FileLoadError()
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["Certificates:Certificate1:Source"] = "File",
+ ["Certificates:Certificate1:Path"] = "Certificate1.pfx",
+ ["Certificates:Certificate1:Password"] = "Password1",
+ ["TestConfig:Certificate"] = "Certificate1"
+ })
+ .Build();
+
+ var certificateFileLoader = new Mock();
+ certificateFileLoader
+ .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny()))
+ .Callback(() => throw new Exception(nameof(Throws_SingleCertificateName_File_FileLoadError)));
+
+ var certificateLoader = new CertificateLoader(
+ configuration.GetSection("Certificates"),
+ null,
+ null,
+ certificateFileLoader.Object,
+ Mock.Of());
+
+ 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]
+ public void Loads_SingleCertificateName_Store()
+ {
+ 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"] = "Certificate1"
+ })
+ .Build();
+
+ var certificate = new X509Certificate2();
+
+ var certificateStoreLoader = new Mock();
+ certificateStoreLoader
+ .Setup(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny()))
+ .Returns(certificate);
+
+ var certificateLoader = new CertificateLoader(
+ configuration.GetSection("Certificates"),
+ null,
+ null,
+ Mock.Of(),
+ certificateStoreLoader.Object);
+
+ var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"));
+ Assert.Equal(1, loadedCertificates.Count());
+ Assert.Same(certificate, loadedCertificates.ElementAt(0));
+ certificateStoreLoader.VerifyAll();
+ }
+
+ [Fact]
+ public void ReturnsNull_SingleCertificateName_Store_NotFoundInStore()
+ {
+ 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"] = "Certificate1"
+ })
+ .Build();
+
+ var certificateStoreLoader = new Mock();
+ certificateStoreLoader
+ .Setup(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny()))
+ .Returns(null);
+
+ var certificateLoader = new CertificateLoader(
+ configuration.GetSection("Certificates"),
+ null,
+ null,
+ Mock.Of(),
+ certificateStoreLoader.Object);
+
+ var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"));
+ Assert.Equal(0, loadedCertificates.Count());
+ certificateStoreLoader.VerifyAll();
+ }
+
+ [Fact]
+ public void Loads_MultipleCertificateNames_File()
+ {
+ 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"] = "Certificate1;Certificate2"
+ })
+ .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"),
+ null,
+ null,
+ certificateFileLoader.Object,
+ Mock.Of());
+
+ var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"));
+ Assert.Equal(2, loadedCertificates.Count());
+ Assert.Same(certificate1, loadedCertificates.ElementAt(0));
+ Assert.Same(certificate2, loadedCertificates.ElementAt(1));
+ certificateFileLoader.VerifyAll();
+ }
+
+ [Theory]
+ [InlineData("Certificate1;Certificate2")]
+ [InlineData("Certificate2;Certificate1")]
+ public void Throws_MultipleCertificateNames_File_FileLoadError(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 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()))
+ .Throws(new Exception(nameof(Throws_MultipleCertificateNames_File_FileLoadError)));
+
+ var certificateLoader = new CertificateLoader(
+ configuration.GetSection("Certificates"),
+ null,
+ null,
+ certificateFileLoader.Object,
+ Mock.Of());
+
+ 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]
+ public void Loads_MultipleCertificateNames_Store()
+ {
+ 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"] = "Certificate1;Certificate2"
+ })
+ .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"),
+ null,
+ null,
+ Mock.Of(),
+ certificateStoreLoader.Object);
+
+ var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"));
+ Assert.Equal(2, loadedCertificates.Count());
+ Assert.Same(certificate1, loadedCertificates.ElementAt(0));
+ Assert.Same(certificate2, loadedCertificates.ElementAt(1));
+ certificateStoreLoader.VerifyAll();
+ }
+
+ [Theory]
+ [InlineData("Certificate1;Certificate2", 1)]
+ [InlineData("Certificate2;Certificate1", 1)]
+ [InlineData("Certificate1;Certificate2;Certificate3", 1)]
+ [InlineData("Certificate2;Certificate3", 0)]
+ [InlineData("Certificate2;Certificate3;Certificate1", 1)]
+ public void ReturnsNull_MultipleCertificateNames_Store_NotFoundInStore(string certificateNames, int expectedFoundCertificates)
+ {
+ 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",
+ ["Certificates:Certificate3:Source"] = "Store",
+ ["Certificates:Certificate3:Subject"] = "notfound.com",
+ ["Certificates:Certificate3:StoreName"] = "Root",
+ ["Certificates:Certificate3:StoreLocation"] = "LocalMachine",
+ ["TestConfig:Certificate"] = certificateNames
+ })
+ .Build();
+
+ var certificateStoreLoader = new Mock();
+ certificateStoreLoader
+ .Setup(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny()))
+ .Returns(new X509Certificate2());
+ certificateStoreLoader
+ .Setup(loader => loader.Load("example.com", "Root", StoreLocation.LocalMachine, It.IsAny()))
+ .Returns(null);
+ certificateStoreLoader
+ .Setup(loader => loader.Load("notfound.com", "Root", StoreLocation.LocalMachine, It.IsAny()))
+ .Returns(null);
+
+ var certificateLoader = new CertificateLoader(
+ configuration.GetSection("Certificates"),
+ null,
+ null,
+ Mock.Of(),
+ certificateStoreLoader.Object);
+
+ var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"));
+ Assert.Equal(expectedFoundCertificates, loadedCertificates.Count());
+ }
+
+ [Fact]
+ public void Loads_MultipleCertificateNames_FileAndStore()
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["Certificates:Certificate1:Source"] = "File",
+ ["Certificates:Certificate1:Path"] = "Certificate1.pfx",
+ ["Certificates:Certificate1:Password"] = "Password1",
+ ["Certificates:Certificate2:Source"] = "Store",
+ ["Certificates:Certificate2:Subject"] = "localhost",
+ ["Certificates:Certificate2:StoreName"] = "My",
+ ["Certificates:Certificate2:StoreLocation"] = "CurrentUser",
+ ["TestConfig:Certificate"] = "Certificate1;Certificate2"
+ })
+ .Build();
+
+ var fileCertificate = new X509Certificate2();
+ var storeCertificate = new X509Certificate2();
+
+ var certificateFileLoader = new Mock();
+ certificateFileLoader
+ .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny()))
+ .Returns(fileCertificate);
+
+ var certificateStoreLoader = new Mock();
+ certificateStoreLoader
+ .Setup(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny()))
+ .Returns(storeCertificate);
+
+ var certificateLoader = new CertificateLoader(
+ configuration.GetSection("Certificates"),
+ null,
+ null,
+ certificateFileLoader.Object,
+ certificateStoreLoader.Object);
+
+ var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"));
+ Assert.Equal(2, loadedCertificates.Count());
+ Assert.Same(fileCertificate, loadedCertificates.ElementAt(0));
+ Assert.Same(storeCertificate, loadedCertificates.ElementAt(1));
+ certificateFileLoader.VerifyAll();
+ certificateStoreLoader.VerifyAll();
+ }
+
+ [Theory]
+ [InlineData("Certificate1;Certificate2;NotFound")]
+ [InlineData("Certificate1;NotFound;Certificate2")]
+ [InlineData("NotFound;Certificate1;Certificate2")]
+ public void Throws_MultipleCertificateNames_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"] = "Store",
+ ["Certificates:Certificate2:Subject"] = "localhost",
+ ["Certificates:Certificate2:StoreName"] = "My",
+ ["Certificates:Certificate2:StoreLocation"] = "CurrentUser",
+ ["TestConfig:Certificate"] = certificateNames
+ })
+ .Build();
+
+ var fileCertificate = new X509Certificate2();
+ var storeCertificate = new X509Certificate2();
+
+ var certificateFileLoader = new Mock();
+ certificateFileLoader
+ .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny()))
+ .Returns(fileCertificate);
+
+ var certificateStoreLoader = new Mock();
+ certificateStoreLoader
+ .Setup(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny()))
+ .Returns(storeCertificate);
+
+ 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 for the current environment.", exception.Message);
+ }
+
+ [Theory]
+ [InlineData("Certificate1;Certificate2")]
+ [InlineData("Certificate2;Certificate1")]
+ public void Throws_MultipleCertificateNames_FileAndStore_FileLoadError(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"] = "Store",
+ ["Certificates:Certificate2:Subject"] = "localhost",
+ ["Certificates:Certificate2:StoreName"] = "My",
+ ["Certificates:Certificate2:StoreLocation"] = "CurrentUser",
+ ["TestConfig:Certificate"] = certificateNames
+ })
+ .Build();
+
+ var storeCertificate = new X509Certificate2();
+
+ var certificateFileLoader = new Mock();
+ certificateFileLoader
+ .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny()))
+ .Throws(new Exception(nameof(Throws_MultipleCertificateNames_FileAndStore_FileLoadError)));
+
+ var certificateStoreLoader = new Mock();
+ certificateStoreLoader
+ .Setup(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny()))
+ .Returns(storeCertificate);
+
+ 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($"Unable to load certificate from file 'Certificate1.pfx'. Error details: '{nameof(Throws_MultipleCertificateNames_FileAndStore_FileLoadError)}'.", exception.Message);
+ }
+
+ [Theory]
+ [InlineData("Certificate1;Certificate2")]
+ [InlineData("Certificate2;Certificate1")]
+ public void ReturnsNull_MultipleCertificateNames_FileAndStore_NotFoundInStore(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"] = "Store",
+ ["Certificates:Certificate2:Subject"] = "localhost",
+ ["Certificates:Certificate2:StoreName"] = "My",
+ ["Certificates:Certificate2:StoreLocation"] = "CurrentUser",
+ ["TestConfig:Certificate"] = "Certificate1;Certificate2"
+ })
+ .Build();
+
+ var certificate = new X509Certificate2();
+
+ var certificateFileLoader = new Mock();
+ certificateFileLoader
+ .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny()))
+ .Returns(certificate);
+
+ var certificateStoreLoader = new Mock();
+ certificateStoreLoader
+ .Setup(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny()))
+ .Returns(null);
+
+ var certificateLoader = new CertificateLoader(
+ configuration.GetSection("Certificates"),
+ null,
+ null,
+ certificateFileLoader.Object,
+ certificateStoreLoader.Object);
+
+ var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"));
+ Assert.Equal(1, loadedCertificates.Count());
+ Assert.Same(certificate, loadedCertificates.ElementAt(0));
+ certificateFileLoader.VerifyAll();
+ certificateStoreLoader.VerifyAll();
+ }
+
+ [Fact]
+ public void Loads_SingleCertificateInline_File()
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["TestConfig:Certificate:Source"] = "File",
+ ["TestConfig:Certificate:Path"] = "Certificate1.pfx",
+ ["TestConfig:Certificate:Password"] = "Password1"
+ })
+ .Build();
+
+ var certificate = new X509Certificate2();
+
+ var certificateFileLoader = new Mock();
+ certificateFileLoader
+ .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny()))
+ .Returns(certificate);
+
+ var certificateLoader = new CertificateLoader(
+ null,
+ null,
+ null,
+ certificateFileLoader.Object,
+ Mock.Of());
+
+ var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"));
+ Assert.Equal(1, loadedCertificates.Count());
+ Assert.Same(certificate, loadedCertificates.ElementAt(0));
+ certificateFileLoader.VerifyAll();
+ }
+
+ [Fact]
+ public void Throws_SingleCertificateInline_FileLoadError()
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["TestConfig:Certificate:Source"] = "File",
+ ["TestConfig:Certificate:Path"] = "Certificate1.pfx",
+ ["TestConfig:Certificate:Password"] = "Password1"
+ })
+ .Build();
+
+ var certificateFileLoader = new Mock();
+ certificateFileLoader
+ .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny()))
+ .Throws(new Exception(nameof(Throws_SingleCertificateInline_FileLoadError)));
+
+ var certificateLoader = new CertificateLoader(
+ null,
+ null,
+ null,
+ certificateFileLoader.Object,
+ Mock.Of());
+
+ 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();
+ }
+
+ [Fact]
+ public void Loads_SingleCertificateInline_Store()
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["TestConfig:Certificate:Source"] = "Store",
+ ["TestConfig:Certificate:Subject"] = "localhost",
+ ["TestConfig:Certificate:StoreName"] = "My",
+ ["TestConfig:Certificate:StoreLocation"] = "CurrentUser",
+ })
+ .Build();
+
+ var certificate = new X509Certificate2();
+
+ var certificateStoreLoader = new Mock();
+ certificateStoreLoader
+ .Setup(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny()))
+ .Returns(certificate);
+
+ var certificateLoader = new CertificateLoader(
+ null,
+ null,
+ null,
+ Mock.Of(),
+ certificateStoreLoader.Object);
+
+ var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"));
+ Assert.Equal(1, loadedCertificates.Count());
+ Assert.Same(certificate, loadedCertificates.ElementAt(0));
+ certificateStoreLoader.VerifyAll();
+ }
+
+ [Fact]
+ public void ReturnsNull_SingleCertificateInline_Store_NotFoundInStore()
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["TestConfig:Certificate:Source"] = "Store",
+ ["TestConfig:Certificate:Subject"] = "localhost",
+ ["TestConfig:Certificate:StoreName"] = "My",
+ ["TestConfig:Certificate:StoreLocation"] = "CurrentUser",
+ })
+ .Build();
+
+ var certificateStoreLoader = new Mock();
+
+ var certificateLoader = new CertificateLoader(
+ null,
+ null,
+ null,
+ Mock.Of(),
+ certificateStoreLoader.Object);
+
+ var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificate"));
+ Assert.Equal(0, loadedCertificates.Count());
+ certificateStoreLoader.Verify(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny()));
+ }
+
+ [Fact]
+ public void Loads_MultipleCertificatesInline_File()
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["TestConfig:Certificates:Certificate1:Source"] = "File",
+ ["TestConfig:Certificates:Certificate1:Path"] = "Certificate1.pfx",
+ ["TestConfig:Certificates:Certificate1:Password"] = "Password1",
+ ["TestConfig:Certificates:Certificate2:Source"] = "File",
+ ["TestConfig:Certificates:Certificate2:Path"] = "Certificate2.pfx",
+ ["TestConfig:Certificates:Certificate2:Password"] = "Password2",
+ })
+ .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(
+ null,
+ null,
+ null,
+ certificateFileLoader.Object,
+ Mock.Of());
+
+ var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificates"));
+ Assert.Equal(2, loadedCertificates.Count());
+ Assert.Same(certificate1, loadedCertificates.ElementAt(0));
+ Assert.Same(certificate2, loadedCertificates.ElementAt(1));
+ certificateFileLoader.VerifyAll();
+ }
+
+ [Fact]
+ public void Throws_MultipleCertificatesInline_File_FileLoadError()
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["TestConfig:Certificates:Certificate1:Source"] = "File",
+ ["TestConfig:Certificates:Certificate1:Path"] = "Certificate1.pfx",
+ ["TestConfig:Certificates:Certificate1:Password"] = "Password1",
+ ["TestConfig:Certificates:Certificate2:Source"] = "File",
+ ["TestConfig:Certificates:Certificate2:Path"] = "Certificate2.pfx",
+ ["TestConfig:Certificates:Certificate2:Password"] = "Password2",
+ })
+ .Build();
+
+ var certificate1 = 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()))
+ .Throws(new Exception(nameof(Throws_MultipleCertificatesInline_File_FileLoadError)));
+
+ var certificateLoader = new CertificateLoader(
+ null,
+ null,
+ null,
+ certificateFileLoader.Object,
+ Mock.Of());
+
+ 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]
+ public void Loads_MultipleCertificatesInline_Store()
+ {
+ 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",
+ ["TestConfig:Certificates:Certificate2:Source"] = "Store",
+ ["TestConfig:Certificates:Certificate2:Subject"] = "example.com",
+ ["TestConfig:Certificates:Certificate2:StoreName"] = "Root",
+ ["TestConfig:Certificates:Certificate2:StoreLocation"] = "LocalMachine"
+ })
+ .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(
+ null,
+ null,
+ null,
+ Mock.Of(),
+ certificateStoreLoader.Object);
+
+ var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificates"));
+ Assert.Equal(2, loadedCertificates.Count());
+ Assert.Same(certificate1, loadedCertificates.ElementAt(0));
+ Assert.Same(certificate2, loadedCertificates.ElementAt(1));
+ certificateStoreLoader.VerifyAll();
+ }
+
+ [Fact]
+ public void ReturnsNull_MultipleCertificatesInline_Store_NotFoundInStore()
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["TestConfig:Certificates:Certificate1:Source"] = "Store",
+ ["TestConfig:Certificates:Certificate1:Subject"] = "notfound.com",
+ ["TestConfig:Certificates:Certificate1:StoreName"] = "Root",
+ ["TestConfig:Certificates:Certificate1:StoreLocation"] = "LocalMachine",
+ ["TestConfig:Certificates:Certificate2:Source"] = "Store",
+ ["TestConfig:Certificates:Certificate2:Subject"] = "localhost",
+ ["TestConfig:Certificates:Certificate2:StoreName"] = "My",
+ ["TestConfig:Certificates:Certificate2:StoreLocation"] = "CurrentUser",
+ ["TestConfig:Certificates:Certificate3:Source"] = "Store",
+ ["TestConfig:Certificates:Certificate3:Subject"] = "example.com",
+ ["TestConfig:Certificates:Certificate3:StoreName"] = "Root",
+ ["TestConfig:Certificates:Certificate3:StoreLocation"] = "LocalMachine"
+ })
+ .Build();
+
+ var certificate = new X509Certificate2();
+
+ var certificateStoreLoader = new Mock();
+ certificateStoreLoader
+ .Setup(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny()))
+ .Returns(certificate);
+
+ var certificateLoader = new CertificateLoader(
+ null,
+ null,
+ null,
+ Mock.Of(),
+ certificateStoreLoader.Object);
+
+ var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificates"));
+ Assert.Equal(1, loadedCertificates.Count());
+ Assert.Same(certificate, loadedCertificates.ElementAt(0));
+ certificateStoreLoader.Verify(loader => loader.Load("notfound.com", "Root", StoreLocation.LocalMachine, It.IsAny()));
+ certificateStoreLoader.Verify(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny()));
+ certificateStoreLoader.Verify(loader => loader.Load("example.com", "Root", StoreLocation.LocalMachine, It.IsAny()));
+ }
+
+ [Fact]
+ public void Loads_MultipleCertificatesInline_FileAndStore()
+ {
+ 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",
+ ["TestConfig:Certificates:Certificate2:Source"] = "File",
+ ["TestConfig:Certificates:Certificate2:Path"] = "Certificate1.pfx",
+ ["TestConfig:Certificates:Certificate2:Password"] = "Password1",
+ ["TestConfig:Certificates:Certificate3:Source"] = "Store",
+ ["TestConfig:Certificates:Certificate3:Subject"] = "example.com",
+ ["TestConfig:Certificates:Certificate3:StoreName"] = "Root",
+ ["TestConfig:Certificates:Certificate3:StoreLocation"] = "LocalMachine",
+ ["TestConfig:Certificates:Certificate4:Source"] = "File",
+ ["TestConfig:Certificates:Certificate4:Path"] = "Certificate2.pfx",
+ ["TestConfig:Certificates:Certificate4:Password"] = "Password2",
+ })
+ .Build();
+
+ var fileCertificate1 = new X509Certificate2();
+ var fileCertificate2 = new X509Certificate2();
+ var storeCertificate1 = new X509Certificate2();
+ var storeCertificate2 = new X509Certificate2();
+
+ var certificateFileLoader = new Mock();
+ certificateFileLoader
+ .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny()))
+ .Returns(fileCertificate1);
+ certificateFileLoader
+ .Setup(loader => loader.Load("Certificate2.pfx", "Password2", It.IsAny()))
+ .Returns(fileCertificate2);
+
+ var certificateStoreLoader = new Mock();
+ certificateStoreLoader
+ .Setup(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny()))
+ .Returns(storeCertificate1);
+ certificateStoreLoader
+ .Setup(loader => loader.Load("example.com", "Root", StoreLocation.LocalMachine, It.IsAny()))
+ .Returns(storeCertificate2);
+
+ var certificateLoader = new CertificateLoader(
+ null,
+ null,
+ null,
+ certificateFileLoader.Object,
+ certificateStoreLoader.Object);
+
+ var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificates"));
+ Assert.Equal(4, loadedCertificates.Count());
+ Assert.Same(storeCertificate1, loadedCertificates.ElementAt(0));
+ Assert.Same(fileCertificate1, loadedCertificates.ElementAt(1));
+ Assert.Same(storeCertificate2, loadedCertificates.ElementAt(2));
+ Assert.Same(fileCertificate2, loadedCertificates.ElementAt(3));
+ certificateStoreLoader.VerifyAll();
+ }
+
+ [Fact]
+ public void Throws_MultipleCertificatesInline_FileAndStore_FileLoadError()
+ {
+ 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",
+ ["TestConfig:Certificates:Certificate2:Source"] = "File",
+ ["TestConfig:Certificates:Certificate2:Path"] = "Certificate1.pfx",
+ ["TestConfig:Certificates:Certificate2:Password"] = "Password1",
+ })
+ .Build();
+
+ var certificate = new X509Certificate2();
+
+ var certificateFileLoader = new Mock();
+ certificateFileLoader
+ .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny()))
+ .Throws(new Exception(nameof(Throws_MultipleCertificatesInline_FileAndStore_FileLoadError)));
+
+ var certificateStoreLoader = new Mock();
+ certificateStoreLoader
+ .Setup(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny()))
+ .Returns(certificate);
+
+ var certificateLoader = new CertificateLoader(
+ null,
+ null,
+ null,
+ certificateFileLoader.Object,
+ certificateStoreLoader.Object);
+
+ 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]
+ public void ReturnsNull_MultipleCertificatesInline_FileAndStore_NotFoundInStore()
+ {
+ 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",
+ ["TestConfig:Certificates:Certificate2:Source"] = "File",
+ ["TestConfig:Certificates:Certificate2:Path"] = "Certificate1.pfx",
+ ["TestConfig:Certificates:Certificate2:Password"] = "Password1",
+ })
+ .Build();
+
+ var certificate = new X509Certificate2();
+
+ var certificateFileLoader = new Mock();
+ certificateFileLoader
+ .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny()))
+ .Returns(certificate);
+
+ var certificateStoreLoader = new Mock();
+ certificateStoreLoader
+ .Setup(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny()))
+ .Returns(null);
+
+ var certificateLoader = new CertificateLoader(
+ null,
+ null,
+ null,
+ certificateFileLoader.Object,
+ certificateStoreLoader.Object);
+
+ var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificates"));
+ 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; }
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Certificates.Configuration.Tests/Microsoft.AspNetCore.Certificates.Configuration.Tests.csproj b/test/Microsoft.AspNetCore.Certificates.Configuration.Tests/Microsoft.AspNetCore.Certificates.Configuration.Tests.csproj
new file mode 100644
index 0000000000..a72b7d129d
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Certificates.Configuration.Tests/Microsoft.AspNetCore.Certificates.Configuration.Tests.csproj
@@ -0,0 +1,32 @@
+
+
+
+
+
+ netcoreapp2.0;net461
+ netcoreapp2.0
+
+
+ true
+ win7-x64
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Microsoft.AspNetCore.Certificates.Configuration.Tests/testCert.pfx b/test/Microsoft.AspNetCore.Certificates.Configuration.Tests/testCert.pfx
new file mode 100644
index 0000000000..7118908c2d
Binary files /dev/null and b/test/Microsoft.AspNetCore.Certificates.Configuration.Tests/testCert.pfx differ