diff --git a/MetaPackages.sln b/MetaPackages.sln index 19e4629..cfcc774 100644 --- a/MetaPackages.sln +++ b/MetaPackages.sln @@ -40,11 +40,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StartRequestDelegateUrlApp" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CreateDefaultBuilderApp", "test\TestSites\CreateDefaultBuilderApp\CreateDefaultBuilderApp.csproj", "{79CF58CE-B020-45D8-BDB5-2D8036BEAD14}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestArtifacts", "TestArtifacts", "{9BBA7A0A-109A-4AC8-B6EF-A52EA7CF1D90}" - ProjectSection(SolutionItems) = preProject - test\TestArtifacts\testCert.pfx = test\TestArtifacts\testCert.pfx - EndProjectSection -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-archive", "src\dotnet-archive\dotnet-archive.csproj", "{AE4216BF-D471-471B-82F3-6B6D004F7D17}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Archive", "src\Microsoft.DotNet.Archive\Microsoft.DotNet.Archive.csproj", "{302400A0-98BB-4C04-88D4-C32DC2D4B945}" @@ -121,7 +116,6 @@ Global {3A85FA52-F601-422E-A42E-9F187DB28492} = {EC22261D-0DE1-47DE-8F7C-072675D6F5B4} {401C741B-6C7C-4E08-9F09-C3D43D22C0DE} = {EC22261D-0DE1-47DE-8F7C-072675D6F5B4} {79CF58CE-B020-45D8-BDB5-2D8036BEAD14} = {EC22261D-0DE1-47DE-8F7C-072675D6F5B4} - {9BBA7A0A-109A-4AC8-B6EF-A52EA7CF1D90} = {9E49B5B9-9E72-42FB-B684-90CA1B1BCF9C} {AE4216BF-D471-471B-82F3-6B6D004F7D17} = {ED834E68-51C3-4ADE-ACC8-6BA6D4207C09} {302400A0-98BB-4C04-88D4-C32DC2D4B945} = {ED834E68-51C3-4ADE-ACC8-6BA6D4207C09} {67E4C92F-6D12-4C52-BB79-B8D11BFC6B82} = {ED834E68-51C3-4ADE-ACC8-6BA6D4207C09} diff --git a/build/dependencies.props b/build/dependencies.props index caea34a..e532f8a 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -5,6 +5,7 @@ 1.0.0-* 4.3.0 2.0.0-* + 4.7.1 10.0.1 15.0.0 2.2.0 diff --git a/samples/AppSettings/appsettings.json b/samples/AppSettings/appsettings.json index da72535..fac810e 100644 --- a/samples/AppSettings/appsettings.json +++ b/samples/AppSettings/appsettings.json @@ -28,7 +28,7 @@ "Source": "File", "Path": "testCert.pfx", // TODO: remove when dotnet user-secrets is working again - "Password": "testPassword", + "Password": "testPassword" } }, // Add testCert.pfx to the current user's certificate store to enable this scenario. diff --git a/src/Microsoft.AspNetCore/CertificateFileLoader.cs b/src/Microsoft.AspNetCore/CertificateFileLoader.cs new file mode 100644 index 0000000..1c0b6e5 --- /dev/null +++ b/src/Microsoft.AspNetCore/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 +{ + 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/CertificateLoader.cs b/src/Microsoft.AspNetCore/CertificateLoader.cs index 9533904..be006b4 100644 --- a/src/Microsoft.AspNetCore/CertificateLoader.cs +++ b/src/Microsoft.AspNetCore/CertificateLoader.cs @@ -12,49 +12,125 @@ namespace Microsoft.AspNetCore /// /// A helper class to load certificates from files and certificate stores based on data. /// - public static class CertificateLoader + public class CertificateLoader { + private readonly IConfiguration _certificatesConfiguration; + private readonly ICertificateFileLoader _certificateFileLoader; + private readonly ICertificateStoreLoader _certificateStoreLoader; + + /// + /// Creates a new instance of . + /// + public CertificateLoader() + : this(null) + { + } + + /// + /// 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()) + { + _certificatesConfiguration = certificatesConfiguration; + } + + internal CertificateLoader(IConfiguration certificatesConfiguration, ICertificateFileLoader certificateFileLoader, ICertificateStoreLoader certificateStoreLoader) + { + _certificatesConfiguration = certificatesConfiguration; + _certificateFileLoader = certificateFileLoader; + _certificateStoreLoader = certificateStoreLoader; + } + + /// + /// 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 one or more certificates from a single source. + /// Loads a certificate by name. /// - /// An with information about a certificate source. - /// The certificate password, in case it's being loaded from a file. - /// The loaded certificates. - public static X509Certificate2 Load(IConfiguration certificateConfiguration, string password) + /// 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()) + { + throw new InvalidOperationException($"No certificate named {certificateName} found in configuration"); + } + + return LoadSingle(certificateConfiguration); + } + + private X509Certificate2 LoadSingle(IConfigurationSection certificateConfiguration) { - var sourceKind = certificateConfiguration.GetValue("Source"); + var sourceKind = certificateConfiguration["Source"]; CertificateSource certificateSource; switch (sourceKind.ToLowerInvariant()) { case "file": - certificateSource = new CertificateFileSource(password); + certificateSource = new CertificateFileSource(_certificateFileLoader); break; case "store": - certificateSource = new CertificateStoreSource(); + certificateSource = new CertificateStoreSource(_certificateStoreLoader); break; default: throw new InvalidOperationException($"Invalid certificate source kind: {sourceKind}"); } certificateConfiguration.Bind(certificateSource); + return certificateSource.Load(); } - /// - /// Loads all certificates specified in an . - /// - /// The root . - /// - /// A dictionary mapping certificate names to loaded certificates. - /// - public static Dictionary LoadAll(IConfiguration configurationRoot) - { - return configurationRoot.GetSection("Certificates").GetChildren() - .ToDictionary( - certificateSource => certificateSource.Key, - certificateSource => Load(certificateSource, certificateSource["Password"])); - } + private IEnumerable LoadMultiple(IConfigurationSection certificatesConfiguration) + => certificatesConfiguration.GetChildren() + .Select(LoadSingle) + .Where(c => c != null); private abstract class CertificateSource { @@ -65,22 +141,24 @@ private abstract class CertificateSource private class CertificateFileSource : CertificateSource { - private readonly string _password; + private ICertificateFileLoader _certificateFileLoader; - public CertificateFileSource(string password) + public CertificateFileSource(ICertificateFileLoader certificateFileLoader) { - _password = password; + _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 +#if NETCOREAPP2_0 ?? TryLoad(X509KeyStorageFlags.EphemeralKeySet, out error) - #endif +#endif ; if (error != null) @@ -95,7 +173,7 @@ private X509Certificate2 TryLoad(X509KeyStorageFlags flags, out Exception except { try { - var loadedCertificate = new X509Certificate2(Path, _password, flags); + var loadedCertificate = _certificateFileLoader.Load(Path, Password, flags); exception = null; return loadedCertificate; } @@ -109,6 +187,13 @@ private X509Certificate2 TryLoad(X509KeyStorageFlags flags, out Exception except private class CertificateStoreSource : CertificateSource { + private readonly ICertificateStoreLoader _certificateStoreLoader; + + public CertificateStoreSource(ICertificateStoreLoader certificateStoreLoader) + { + _certificateStoreLoader = certificateStoreLoader; + } + public string Subject { get; set; } public string StoreName { get; set; } public string StoreLocation { get; set; } @@ -121,52 +206,7 @@ public override X509Certificate2 Load() throw new InvalidOperationException($"Invalid store location: {StoreLocation}"); } - 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: !AllowInvalid); - foundCertificate = foundCertificates - .OfType() - .OrderByDescending(certificate => certificate.NotAfter) - .FirstOrDefault(); - - if (foundCertificate == null) - { - throw new InvalidOperationException($"No certificate found for {Subject} in store {StoreName} in {StoreLocation}"); - } - - 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(); - } - } + return _certificateStoreLoader.Load(Subject, StoreName, storeLocation, !AllowInvalid); } } } diff --git a/src/Microsoft.AspNetCore/CertificateStoreLoader.cs b/src/Microsoft.AspNetCore/CertificateStoreLoader.cs new file mode 100644 index 0000000..27158d4 --- /dev/null +++ b/src/Microsoft.AspNetCore/CertificateStoreLoader.cs @@ -0,0 +1,56 @@ +// 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 +{ + internal class CertificateStoreLoader : ICertificateStoreLoader + { + 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/ICertificateFileLoader.cs b/src/Microsoft.AspNetCore/ICertificateFileLoader.cs new file mode 100644 index 0000000..4c394bb --- /dev/null +++ b/src/Microsoft.AspNetCore/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 +{ + internal interface ICertificateFileLoader + { + X509Certificate2 Load(string path, string password, X509KeyStorageFlags flags); + } +} diff --git a/src/Microsoft.AspNetCore/ICertificateStoreLoader.cs b/src/Microsoft.AspNetCore/ICertificateStoreLoader.cs new file mode 100644 index 0000000..f128bbd --- /dev/null +++ b/src/Microsoft.AspNetCore/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 +{ + internal interface ICertificateStoreLoader + { + X509Certificate2 Load(string subject, string storeName, StoreLocation storeLocation, bool validOnly); + } +} diff --git a/src/Microsoft.AspNetCore/KestrelServerOptionsSetup.cs b/src/Microsoft.AspNetCore/KestrelServerOptionsSetup.cs index 888ece3..d2ec6f8 100644 --- a/src/Microsoft.AspNetCore/KestrelServerOptionsSetup.cs +++ b/src/Microsoft.AspNetCore/KestrelServerOptionsSetup.cs @@ -2,39 +2,24 @@ // 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.Net; -using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore { - /// - /// Binds Kestrel configuration. - /// - public class KestrelServerOptionsSetup : IConfigureOptions + internal class KestrelServerOptionsSetup : IConfigureOptions { private readonly IConfiguration _configurationRoot; - /// - /// Creates a new instance of . - /// - /// The root . public KestrelServerOptionsSetup(IConfiguration configurationRoot) { _configurationRoot = configurationRoot; } - /// - /// Configures a instance. - /// - /// The to configure. public void Configure(KestrelServerOptions options) { BindConfiguration(options); @@ -42,60 +27,46 @@ public void Configure(KestrelServerOptions options) private void BindConfiguration(KestrelServerOptions options) { - var certificates = CertificateLoader.LoadAll(_configurationRoot); - var endPoints = _configurationRoot.GetSection("Kestrel:EndPoints"); + var certificateLoader = new CertificateLoader(_configurationRoot.GetSection("Certificates")); - foreach (var endPoint in endPoints.GetChildren()) + foreach (var endPoint in _configurationRoot.GetSection("Kestrel:EndPoints").GetChildren()) { - BindEndPoint(options, endPoint, certificates); + BindEndPoint(options, endPoint, certificateLoader); } } private void BindEndPoint( KestrelServerOptions options, IConfigurationSection endPoint, - Dictionary certificates) + CertificateLoader certificateLoader) { - var addressValue = endPoint.GetValue("Address"); - var portValue = endPoint.GetValue("Port"); + var configAddress = endPoint.GetValue("Address"); + var configPort = endPoint.GetValue("Port"); - IPAddress address; - if (!IPAddress.TryParse(addressValue, out address)) + if (!IPAddress.TryParse(configAddress, out var address)) { - throw new InvalidOperationException($"Invalid IP address: {addressValue}"); + throw new InvalidOperationException($"Invalid IP address in configuration: {configAddress}"); } - int port; - if (!int.TryParse(portValue, out port)) + if (!int.TryParse(configPort, out var port)) { - throw new InvalidOperationException($"Invalid port: {portValue}"); + throw new InvalidOperationException($"Invalid port in configuration: {configPort}"); } options.Listen(address, port, listenOptions => { - var certificateName = endPoint.GetValue("Certificate"); + var certificateConfig = endPoint.GetSection("Certificate"); - X509Certificate2 endPointCertificate = null; - if (certificateName != null) + if (certificateConfig.Exists()) { - if (!certificates.TryGetValue(certificateName, out endPointCertificate)) - { - throw new InvalidOperationException($"No certificate named {certificateName} found in configuration"); - } - } - else - { - var certificate = endPoint.GetSection("Certificate"); + var certificate = certificateLoader.Load(certificateConfig).FirstOrDefault(); - if (certificate.GetChildren().Any()) + if (certificate == null) { - endPointCertificate = CertificateLoader.Load(certificate, certificate["Password"]); + throw new InvalidOperationException($"Unable to load certificate for endpoint '{endPoint.Key}'"); } - } - if (endPointCertificate != null) - { - listenOptions.UseHttps(endPointCertificate); + listenOptions.UseHttps(certificate); } }); } diff --git a/src/Microsoft.AspNetCore/Microsoft.AspNetCore.csproj b/src/Microsoft.AspNetCore/Microsoft.AspNetCore.csproj index 5350bf9..5841f9d 100644 --- a/src/Microsoft.AspNetCore/Microsoft.AspNetCore.csproj +++ b/src/Microsoft.AspNetCore/Microsoft.AspNetCore.csproj @@ -3,7 +3,7 @@ - netstandard1.3 + netstandard1.3;netcoreapp2.0 aspnetcore Microsoft.AspNetCore true diff --git a/src/Microsoft.AspNetCore/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a3a00f9 --- /dev/null +++ b/src/Microsoft.AspNetCore/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.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.FunctionalTests/CertificateLoaderTests.cs b/test/Microsoft.AspNetCore.FunctionalTests/CertificateLoaderTests.cs new file mode 100644 index 0000000..7cf3787 --- /dev/null +++ b/test/Microsoft.AspNetCore.FunctionalTests/CertificateLoaderTests.cs @@ -0,0 +1,999 @@ +// 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 Xunit; +using Moq; + +namespace Microsoft.AspNetCore.FunctionalTests +{ + 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"), + 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_File_KeyNotFound() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Certificates:Certificate1:Source"] = "File", + ["Certificates:Certificate1:Path"] = "Certificate1.pfx", + ["Certificates:Certificate1:Password"] = "Password1", + ["TestConfig:Certificate"] = "Certificate2" + }) + .Build(); + + var certificate = new X509Certificate2(); + + 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 Throws_SingleCertificateName_File_FileNotFound() + { + 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 exception = new Exception(); + + var certificateFileLoader = new Mock(); + certificateFileLoader + .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny())) + .Callback(() => throw exception); + + var certificateLoader = new CertificateLoader( + configuration.GetSection("Certificates"), + certificateFileLoader.Object, + Mock.Of()); + + Assert.Same(exception, Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")))); + } + + [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"), + 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 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() + { + 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"), + 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"), + 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;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) + { + 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 exception = new Exception(); + + 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(exception); + + var certificateLoader = new CertificateLoader( + configuration.GetSection("Certificates"), + certificateFileLoader.Object, + Mock.Of()); + + Assert.Same(exception, Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")))); + } + + [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"), + 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;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)] + [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"), + 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"), + 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_FileAndStore_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"), + 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); + } + + [Theory] + [InlineData("Certificate1;Certificate2")] + [InlineData("Certificate2;Certificate1")] + public void Throws_MultipleCertificateNames_FileAndStore_FileNotFound(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 exception = new Exception(); + var storeCertificate = new X509Certificate2(); + + var certificateFileLoader = new Mock(); + certificateFileLoader + .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny())) + .Throws(exception); + + var certificateStoreLoader = new Mock(); + certificateStoreLoader + .Setup(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny())) + .Returns(storeCertificate); + + var certificateLoader = new CertificateLoader( + configuration.GetSection("Certificates"), + certificateFileLoader.Object, + certificateStoreLoader.Object); + + Assert.Same(exception, Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")))); + } + + [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"), + 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, + 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_FileNotFound() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["TestConfig:Certificate:Source"] = "File", + ["TestConfig:Certificate:Path"] = "Certificate1.pfx", + ["TestConfig:Certificate:Password"] = "Password1" + }) + .Build(); + + var exception = new Exception(); + + var certificateFileLoader = new Mock(); + certificateFileLoader + .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny())) + .Throws(exception); + + var certificateLoader = new CertificateLoader( + null, + certificateFileLoader.Object, + Mock.Of()); + + Assert.Same(exception, Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificate")))); + 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, + 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, + 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, + 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_FileNotFound() + { + 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 exception = new Exception(); + + 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(exception); + + var certificateLoader = new CertificateLoader( + null, + certificateFileLoader.Object, + Mock.Of()); + + Assert.Same(exception, Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificates")))); + } + + [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, + 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, + 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, + 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_FileNotFound() + { + 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 exception = new Exception(); + var certificate = new X509Certificate2(); + + var certificateFileLoader = new Mock(); + certificateFileLoader + .Setup(loader => loader.Load("Certificate1.pfx", "Password1", It.IsAny())) + .Throws(exception); + + var certificateStoreLoader = new Mock(); + certificateStoreLoader + .Setup(loader => loader.Load("localhost", "My", StoreLocation.CurrentUser, It.IsAny())) + .Returns(certificate); + + var certificateLoader = new CertificateLoader( + null, + certificateFileLoader.Object, + certificateStoreLoader.Object); + + Assert.Same(exception, Assert.Throws(() => certificateLoader.Load(configuration.GetSection("TestConfig:Certificates")))); + } + + [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, + certificateFileLoader.Object, + certificateStoreLoader.Object); + + var loadedCertificates = certificateLoader.Load(configuration.GetSection("TestConfig:Certificates")); + Assert.Equal(1, loadedCertificates.Count()); + Assert.Same(certificate, loadedCertificates.ElementAt(0)); + } + } +} diff --git a/test/Microsoft.AspNetCore.FunctionalTests/Microsoft.AspNetCore.FunctionalTests.csproj b/test/Microsoft.AspNetCore.FunctionalTests/Microsoft.AspNetCore.FunctionalTests.csproj index a4676dc..13d57e6 100644 --- a/test/Microsoft.AspNetCore.FunctionalTests/Microsoft.AspNetCore.FunctionalTests.csproj +++ b/test/Microsoft.AspNetCore.FunctionalTests/Microsoft.AspNetCore.FunctionalTests.csproj @@ -7,7 +7,7 @@ - + @@ -17,6 +17,7 @@ + diff --git a/test/Microsoft.AspNetCore.FunctionalTests/TestArtifacts/Certificate.pfx b/test/Microsoft.AspNetCore.FunctionalTests/TestArtifacts/Certificate.pfx new file mode 100644 index 0000000..c792c52 Binary files /dev/null and b/test/Microsoft.AspNetCore.FunctionalTests/TestArtifacts/Certificate.pfx differ diff --git a/test/Microsoft.AspNetCore.FunctionalTests/WebHostFunctionalTests.cs b/test/Microsoft.AspNetCore.FunctionalTests/WebHostFunctionalTests.cs index 43be04f..00e1fbf 100644 --- a/test/Microsoft.AspNetCore.FunctionalTests/WebHostFunctionalTests.cs +++ b/test/Microsoft.AspNetCore.FunctionalTests/WebHostFunctionalTests.cs @@ -134,8 +134,7 @@ public async Task BindsKestrelHttpsEndPointFromConfiguration_ReferencedCertifica ""Certificates"": { ""TestCert"": { ""Source"": ""File"", - ""Path"": ""testCert.pfx"", - ""Password"": ""testPassword"" + ""Path"": ""TestArtifacts/Certificate.pfx"" } } } @@ -173,8 +172,7 @@ public async Task BindsKestrelHttpsEndPointFromConfiguration_InlineCertificateFi ""Port"": 0, ""Certificate"": { ""Source"": ""File"", - ""Path"": ""testCert.pfx"", - ""Password"": ""testPassword"" + ""Path"": ""TestArtifacts/Certificate.pfx"", } } } diff --git a/test/TestArtifacts/testCert.pfx b/test/TestArtifacts/testCert.pfx deleted file mode 100644 index 7118908..0000000 Binary files a/test/TestArtifacts/testCert.pfx and /dev/null differ