From 605aeddc227a1503e8e78031755d081a5ce0958f Mon Sep 17 00:00:00 2001 From: Cesar Blum Silveira Date: Mon, 1 May 2017 18:05:11 -0700 Subject: [PATCH] Support more certificate loading scenarios (#69). --- MetaPackages.sln | 6 - build/dependencies.props | 1 + samples/AppSettings/appsettings.json | 2 +- .../CertificateFileLoader.cs | 15 + src/Microsoft.AspNetCore/CertificateLoader.cs | 190 ++-- .../CertificateStoreLoader.cs | 56 + .../ICertificateFileLoader.cs | 12 + .../ICertificateStoreLoader.cs | 12 + .../KestrelServerOptionsSetup.cs | 65 +- .../Microsoft.AspNetCore.csproj | 2 +- .../Properties/AssemblyInfo.cs | 7 + .../CertificateLoaderTests.cs | 999 ++++++++++++++++++ ...icrosoft.AspNetCore.FunctionalTests.csproj | 3 +- .../TestArtifacts/Certificate.pfx | Bin 0 -> 2461 bytes .../WebHostFunctionalTests.cs | 6 +- test/TestArtifacts/testCert.pfx | Bin 2483 -> 0 bytes 16 files changed, 1241 insertions(+), 135 deletions(-) create mode 100644 src/Microsoft.AspNetCore/CertificateFileLoader.cs create mode 100644 src/Microsoft.AspNetCore/CertificateStoreLoader.cs create mode 100644 src/Microsoft.AspNetCore/ICertificateFileLoader.cs create mode 100644 src/Microsoft.AspNetCore/ICertificateStoreLoader.cs create mode 100644 src/Microsoft.AspNetCore/Properties/AssemblyInfo.cs create mode 100644 test/Microsoft.AspNetCore.FunctionalTests/CertificateLoaderTests.cs create mode 100644 test/Microsoft.AspNetCore.FunctionalTests/TestArtifacts/Certificate.pfx delete mode 100644 test/TestArtifacts/testCert.pfx diff --git a/MetaPackages.sln b/MetaPackages.sln index 19e46291d0..cfcc774203 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 caea34a061..e532f8a25d 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 da7253554d..fac810ec12 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 0000000000..1c0b6e5759 --- /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 95339040a1..be006b4941 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; + /// - /// Loads one or more certificates from a single source. + /// Creates a new instance of . /// - /// 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) + public CertificateLoader() + : this(null) { - var sourceKind = certificateConfiguration.GetValue("Source"); + } + + /// + /// 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 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()) + { + throw new InvalidOperationException($"No certificate named {certificateName} found in configuration"); + } + + return LoadSingle(certificateConfiguration); + } + + private X509Certificate2 LoadSingle(IConfigurationSection certificateConfiguration) + { + 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 @@ namespace Microsoft.AspNetCore 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 @@ namespace Microsoft.AspNetCore { try { - var loadedCertificate = new X509Certificate2(Path, _password, flags); + var loadedCertificate = _certificateFileLoader.Load(Path, Password, flags); exception = null; return loadedCertificate; } @@ -109,6 +187,13 @@ namespace Microsoft.AspNetCore 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 @@ namespace Microsoft.AspNetCore 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 0000000000..27158d4da3 --- /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 0000000000..4c394bbd02 --- /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 0000000000..f128bbd675 --- /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 888ece3903..d2ec6f8a4a 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 @@ namespace Microsoft.AspNetCore 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)) + var certificate = certificateLoader.Load(certificateConfig).FirstOrDefault(); + + if (certificate == null) { - throw new InvalidOperationException($"No certificate named {certificateName} found in configuration"); + throw new InvalidOperationException($"Unable to load certificate for endpoint '{endPoint.Key}'"); } - } - else - { - var certificate = endPoint.GetSection("Certificate"); - if (certificate.GetChildren().Any()) - { - endPointCertificate = CertificateLoader.Load(certificate, certificate["Password"]); - } - } - - 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 5350bf93a3..5841f9dc24 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 0000000000..a3a00f9d5c --- /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 0000000000..7cf3787a56 --- /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 a4676dcc7f..13d57e6616 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 0000000000000000000000000000000000000000..c792c522488adf723e6f37b354a9f4178dedf98d GIT binary patch literal 2461 zcmV;O31apzf(e-d0Ru3C310>YDuzgg_YDCD0ic2jPy~VrOfZ58NHBr}{{{&vhDe6@ z4FLxRpn?PNFoFZ@0s#Opf&=9S2`Yw2hW8Bt2LUh~1_~;MNQUCLhc?KpZ79UX((b9gtz3l$ShHTW^w(4j=OB4)=~cBICF-(;4ox6qbLYN7}GC8?wNZs!fw~MG~CeO>3{q`?$!YE&|TW)7%ltY`g`J(4!EA+0qG<5f-@An+sS2s zyKPh^XzQo6k{=!X&J>f&qlC6rk8nC2#J1jlBp_18$8vKh-U$SOlzOIq)FBRBa&tg` zR?M@NvBD)UfX4@ypWaR=ez3Y(XT=|lyg$RizAl06K8 zr#J2k1>y5RV4K1`d870`A3QB{3^91xcGGJjnA z?o&nSF>%B)m;bj~^X${eq1{=%dD7bv41prq(x@sC@C{Ux|CklhNTJsPwbdHcnU6aO z_MGTcs3e!*5A|F5ucqU|`7#}Z*Khdo>V`x1Uo*CTvKwpEx#qsTud0(OiO61wr zWsY)LxoLde@Ttox)W3{NslcIT{a`vbL7X6WFHg+d7${@_O!$w4$&u67int_=`alI=5ZenfB@%ode@Q;fL^<;&t0j74IM#t*%O&Eb#E zO3Q0JLna^Ynq)c|cy-Ob09lVVleD}Lc6330Fpna<#uA`2$#oKNgzXM0hIDWxb(s$V z=mM&<1Vvca4{@Gq&+gV^)-i68+iSlE)4C9T6{pXBVY49f&hbPcQJn0EC!Fh`F_y+b z8JRS!vsd+UUsgd_z02z(#_2S|4Opb0xM2A#FoFd^1_>&LNQU<1f~E*r4=p<8OP?$r5q@pf%h z&EDD8e?4ydKLhh0X1jw|iGk*vgA=^gX0p1AZNR#zCAo>PbBd8-BIOL%BFE;!g*H$B z+%|ma9v2wNKO`Lk-c@XG95iEwCmv=hC0hfNaJ5Y?Vzl4P_ACkn6A6(*Xt)~gr24?& zc+?9X2d*291gesBlP(J`eTUNdobKwi*3?i~M`)ta3fK@;q{PEK+IrL7J&qIj20hwK z=FjLbo?5-}nuEo?0O*EM;WroV6@Y;9(0u=E0oZAnYKUmZgO@*BlUf&!yrg|^ZnYQS zWi0?G-9lB32QH2trNhMsRKwW8rzbJjF~4~>BJmD-z4*Ji`w!)TEIh(gI>dnMN`R$Z z=Jikm=|Vhkte489k2e;D)Mg|#af_PODc_GM$G;+R^1ik|-HCl-JgeBtM+o5lF|Di9xq1Z@Wssz@fb zbPnrus>$DBXV7xnJ^E#mF2Lg}$J%^TNrn2?I(t>5fuj^+cY|SyN8kR5EF23+oZTj1 z%$vAm>Ub#v)EGLri#9f_hQ_f2!3bDH)r&Jn${QBL?(7GS(_D`PTw67D>K*P2w4Q_&JRoj>7m zaA93;0I>mecVE^YkUui2b^mZQYw
=S3wwEn5^#$v~O)z)+%fl)_B{=vHjXvs{( zy@6^1ky#X!=Zw<(^`EROArA_6(b|7TC=s=GKFsVpF)PM*r(PTw&lG0&x0Io%!U9;2 zj=jihQxIh2UeSG6S6;zp+6D@@%7}M0&G{8oj}eIoFaW(cX}X(GB;Yqp2oNC;js&>K z4!MD=3=^sWTzc;ZOTmMxdB~e+wFY(hV*xIg)EC;wsZ`%SkjAUb@p2GYltVU5Ttp*T z(tN%{T<*Po);<(6l9pKk*YlK@3HVebh|1Js+1m~5UKrYT>M%KIi~nwzO{^ybk#t*r zNHqrglnI(D8rfWhnHTqMP8URj$C@;Qkk-JZ?!hcI;l{vNH9t+iV;tU}1irx}EWzGn zOf)IR=b-k6fsXnc*VTCl@&%|PujOc*)#Ii{f5SuZ-iR#-b2pc=YG2>2FDZ`JQ0=KS z;ZfPMXPehqQ>l~pxKFs91$jFd6km}OdnDGgAq4W!CMv_%kJO8i?vwn_V4kLz?&j|7 zi}in{dI)eyF*ULRbHg&bPFRx#AI%8;3jw~+y|_AIZ$(m2xa~0|Fe3&DDuzgg_YDCF z6)_eB6ng)VNS~j_hgq$}Xf4Qu|9}4v;V>~UAutIB1uG5%0vZJX1Qd*Ji=f=R`J3~d bWr6|Fzx*IOqG<#OPZEU6;7((40s;sCZpw)y literal 0 HcmV?d00001 diff --git a/test/Microsoft.AspNetCore.FunctionalTests/WebHostFunctionalTests.cs b/test/Microsoft.AspNetCore.FunctionalTests/WebHostFunctionalTests.cs index 43be04f16e..00e1fbfba6 100644 --- a/test/Microsoft.AspNetCore.FunctionalTests/WebHostFunctionalTests.cs +++ b/test/Microsoft.AspNetCore.FunctionalTests/WebHostFunctionalTests.cs @@ -134,8 +134,7 @@ namespace Microsoft.AspNetCore.Tests ""Certificates"": { ""TestCert"": { ""Source"": ""File"", - ""Path"": ""testCert.pfx"", - ""Password"": ""testPassword"" + ""Path"": ""TestArtifacts/Certificate.pfx"" } } } @@ -173,8 +172,7 @@ namespace Microsoft.AspNetCore.Tests ""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 7118908c2d730670c16e9f8b2c532a262c951989..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2483 zcmaKuc|27A8pqF>IWr86E&Q@(n=B)p$ug!;QVB6xij*z;uPLG!yCz#DQB)+9G$9m9 zQU)=DWXU?*EZIwG!+0d++P@yZ4Xhoagg?p6B~|Ue7tN=Ny=UD?x#1n1MTq z#c9MHh+D#gd|(a(cN}8i91v^=GcdgW3SmA$49p~gM-dys3jVWdg8+!iVL)pz1LDE5 zSb=|GAn(@R=(Ux!MfS9@}sFu-xDd zIt2+mqSq$glwy_6UNs<2?(qERU!gJ;5j}Pp&6trxG=wi)=@k(w2+fJVnc+qvXVzy(>Om4;L|^)R`t*3nTpAmEmTl(#i!RV#a0t#u6>Q9mY`-Nmcs7$XjXT7 zUmCD`O~_j7!%R#I?cG-7C^hcH)@l?WC1vyw$FFu_(r)jhOq6p}W8sG7NO{YTy8tG4 zrb$tTkag*G?(7lfoGx$4YWui>{{@}-FB2ub=}RX{1zx?j)s-##J9|G7E1@-;7Nuln z9MQoX7FJ76+D#XXT@ZZmLZCufIdf3@OigG6m8I7!GT=7VD|>?6e!z9=eT}*E_tSn6 zl+clHCZ-kcIR#gen#LjMJW8>0QtViaQB#FhqsCb0YPYr3;jRITl@V9Aph24D?r2d` zetCyyCg<*O-u+M& zW^ptmT|}p$VAOZpmbQ1{5fK-6ytEvre#Po}6c2URn`viQAF2+e?Z~PK2&pd>7=7)I zTCYm)@3PFRu_6a6Kb)IpCzQ%e3l%O#SDA+$Pq{Dk{HCqi7z>qd{nVpebffL7h{c4( zmhXn~G+C27S3(IfC)q2KON=YwqHXEo%zc40DgWLzF{%RIdr@RcLu90qMSHf!Y}JaqP<={8_Rfe;ddR5= zKEo;^Yip&^m((#{czE{kUga3-@`*;&EwO}Jt>QdURP2P>ob^j-A!qld-0S_pm)kjs zkNo48oZnMt){W~o8g^f;4#?lRLr-T@f}wH1o~-Iq=NEVtTVEZ`vrW~!>2yh%;Bc~H zHl&OK>n@d`*e19*9#v>zZpU?I);f7}IPIfSSk#N|ujE492Itg)l!)TJ19@FE^x|p= zH16NC7OfK&|6_!AnWfTIf^YPOa&`|nbk3VR0vql6&s@y1V3QOU%(`Re+kJgrz?r9!{^wOQ4W-eng23gc}f(LxIs zH_Ls~5izbjcRQH#WH6s6hR;zn>j_R8aJ$A)6xNneu8UI-vWV8Z@HZu&WwvG5q{1ZS zdZeVf{Pv5-u281~y;aJe*x%Uv0@biMZ$vPbKj}O`(SOWQc~kJX` zXR&d4DtAe@2RH$^ z0os5*;0eIUeJi3Uh`A%44x(XzjClG8BO~-r_A}odiRuHo2-86#`mhrgN5p~<$RLY? zq(kynfFA5{v#p+EA1 z5aoe1763EQHorRm`C&ktKn(OQ1n)$Q{GZz&jRb`eDEMpl<0O#+)DMV(T7nsIzCG{QuM->B9g7Lrl2SE&gW`M!~(un|y0fIn=b^6_$ z9{zEzgYI~39xn0ZP*9qBL%fg7rg$ttt&TOmvfNNO<6FT0ZavM$Y4CYLQGIcIYv9Y& zBGPUh&QTfW;V2!)oIra@s&d968y-y}Y|ww(R$GzWS*V&)k@W0>Slem{|HdTCjm;_5 zwY*A8W3nUbemE^_f0ng$tbd<`sr?TO-_&VCw+F#7P@LkIl$1PzTBoPY1b88EIO>UO zP-NK7+g2yD3U6g3i|iA6+su>54sf_Sk0F=)1|9odnCM4u2Rs z=&Y?-V&VquSN%3FJ2~ZGweP~iLs|w=l@9yu$tj@}Dp?e-2JUsqOoswdXb=E%&0te_ zA2M+{5Hf-dqD7=yw*r@A*xkn(1IS~nfP}k}e?4Bt|9g(eph4hFX_|S6nj1&Sz9z^= zRw~<&-9d@FzTn6S*RVE{Wj5lgLJr9HLB8S9CgOm*>XA8*y4`JE;^s$=bqD#U4;e5C&x&ggKIAVL zrQ)Yd8|{>7Z(6*B&7&4&9(*vDOfHMuR-Dk1IZia*XM^EZUD^{?cWG>J>KrtElc*{K zaVl(7SN2cH4I6Q$bZOpJ8e5LKaG7p;?tJ~#+9QrTYU@f#5`Vo7cEX!szCT}iX-K^2 w#3o+=C+lQz2J+SOEzVX(eJ)e7=eicC{rr9U2VGDcdH?_b