From 8d315380bb744b30901be11db537ae89929766ac Mon Sep 17 00:00:00 2001 From: nico_dreylaq Date: Wed, 4 Dec 2024 10:41:57 +0100 Subject: [PATCH] refactor: remove localTrustStore and implem sslConfig --- .../src/ArmoniK.Core.Adapters.Amqp.csproj | 1 + Adaptors/Amqp/src/QueueBuilder.cs | 98 +++++++++++--- Adaptors/RabbitMQ/src/QueueBuilder.cs | 117 ++++++++++++----- Adaptors/Redis/src/ServiceCollectionExt.cs | 123 ++++++++++++------ 4 files changed, 243 insertions(+), 96 deletions(-) diff --git a/Adaptors/Amqp/src/ArmoniK.Core.Adapters.Amqp.csproj b/Adaptors/Amqp/src/ArmoniK.Core.Adapters.Amqp.csproj index 6e306fa25..f1c6ae4d4 100644 --- a/Adaptors/Amqp/src/ArmoniK.Core.Adapters.Amqp.csproj +++ b/Adaptors/Amqp/src/ArmoniK.Core.Adapters.Amqp.csproj @@ -35,6 +35,7 @@ false runtime + diff --git a/Adaptors/Amqp/src/QueueBuilder.cs b/Adaptors/Amqp/src/QueueBuilder.cs index 92308a981..ccc2016d2 100644 --- a/Adaptors/Amqp/src/QueueBuilder.cs +++ b/Adaptors/Amqp/src/QueueBuilder.cs @@ -15,7 +15,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -using System; +using System.Linq; +using System.Net.Security; using System.Security.Cryptography.X509Certificates; using ArmoniK.Core.Base; @@ -27,6 +28,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using RabbitMQ.Client; + namespace ArmoniK.Core.Adapters.Amqp; /// @@ -67,26 +70,79 @@ public void Build(IServiceCollection serviceCollection, if (!string.IsNullOrEmpty(amqpOptions.CaPath)) { - var localTrustStore = new X509Store(StoreName.Root); - var certificateCollection = new X509Certificate2Collection(); - try - { - certificateCollection.ImportFromPemFile(amqpOptions.CaPath); - localTrustStore.Open(OpenFlags.ReadWrite); - localTrustStore.AddRange(certificateCollection); - logger.LogTrace("Imported AMQP certificate from file {path}", - amqpOptions.CaPath); - } - catch (Exception ex) - { - logger.LogError("Root certificate import failed: {error}", - ex.Message); - throw; - } - finally - { - localTrustStore.Close(); - } + var authority = new X509Certificate2(amqpOptions.CaPath); + + // Configure the SSL settings + var sslOption = new SslOption + { + Enabled = true, + ServerName = amqpOptions.Host, + Certs = new X509Certificate2Collection(), + AcceptablePolicyErrors = SslPolicyErrors.RemoteCertificateChainErrors, + CertificateValidationCallback = (sender, + certificate, + chain, + sslPolicyErrors) => + { + if (sslPolicyErrors == SslPolicyErrors.None) + { + return true; + } + + if ((sslPolicyErrors & ~SslPolicyErrors.RemoteCertificateChainErrors) != 0) + { + logger.LogError("SSL validation failed: {errors}", + sslPolicyErrors); + return false; + } + + // If there is any error other than untrusted root or partial chain, fail the validation + if (chain!.ChainStatus.Any(status => status.Status is not X509ChainStatusFlags.UntrustedRoot and + not X509ChainStatusFlags.PartialChain)) + { + return false; + } + + // Disable some extensive checks that would fail on the authority that is not in store + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; + + // Add unknown authority to the store + chain.ChainPolicy.ExtraStore.Add(authority); + + // Check if the chain is valid for the actual server certificate (ie: trusted) + if (!chain.Build(new X509Certificate2(certificate!))) + { + logger.LogError("SSL chain validation failed."); + return false; + } + + // Check that the chain root is actually the specified authority (caCert) + var isTrusted = chain.ChainElements.Any(x => x.Certificate.Thumbprint == authority.Thumbprint); + + if (!isTrusted) + { + logger.LogError("Certificate chain root does not match the specified CA authority."); + } + + return isTrusted; + }, + }; + + // Apply the SSL settings to your RabbitMQ connection factory + var factory = new ConnectionFactory + { + HostName = amqpOptions.Host, + UserName = amqpOptions.User, + Password = amqpOptions.Password, + Ssl = sslOption, + }; + + serviceCollection.AddSingleton(factory); + } + else + { + logger.LogTrace("No CA path provided"); } serviceCollection.AddSingletonWithHealthCheck(nameof(IConnectionAmqp)); diff --git a/Adaptors/RabbitMQ/src/QueueBuilder.cs b/Adaptors/RabbitMQ/src/QueueBuilder.cs index 12238c103..7f746d03b 100644 --- a/Adaptors/RabbitMQ/src/QueueBuilder.cs +++ b/Adaptors/RabbitMQ/src/QueueBuilder.cs @@ -15,10 +15,11 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -using System; +using System.Linq; +using System.Net.Security; using System.Security.Cryptography.X509Certificates; -using ArmoniK.Core.Adapters.QueueCommon; +using ArmoniK.Core.Adapters.RabbitMQ; using ArmoniK.Core.Base; using ArmoniK.Core.Utils; @@ -28,7 +29,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace ArmoniK.Core.Adapters.RabbitMQ; +using RabbitMQ.Client; + +namespace ArmoniK.Core.Adapters.Amqp; /// /// Class for building RabbitMQ object and Queue interfaces through Dependency Injection @@ -42,25 +45,22 @@ public void Build(IServiceCollection serviceCollection, ConfigurationManager configuration, ILogger logger) { - logger.LogInformation("Configure RabbitMQ client"); - + logger.LogInformation("Configure Amqp client"); serviceCollection.AddOption(configuration, - Amqp.SettingSection, - out Amqp amqpOptions); - + QueueCommon.Amqp.SettingSection, + out QueueCommon.Amqp amqpOptions); if (!string.IsNullOrEmpty(amqpOptions.CredentialsPath)) { configuration.AddJsonFile(amqpOptions.CredentialsPath, false, false); - logger.LogTrace("Loaded amqp credentials from file {path}", - amqpOptions.CredentialsPath); - serviceCollection.AddOption(configuration, - Amqp.SettingSection, + QueueCommon.Amqp.SettingSection, out amqpOptions); + logger.LogTrace("Loaded amqp credentials from file {path}", + amqpOptions.CredentialsPath); } else { @@ -69,26 +69,79 @@ public void Build(IServiceCollection serviceCollection, if (!string.IsNullOrEmpty(amqpOptions.CaPath)) { - var localTrustStore = new X509Store(StoreName.Root); - var certificateCollection = new X509Certificate2Collection(); - try - { - certificateCollection.ImportFromPemFile(amqpOptions.CaPath); - localTrustStore.Open(OpenFlags.ReadWrite); - localTrustStore.AddRange(certificateCollection); - logger.LogTrace("Imported AMQP certificate from file {path}", - amqpOptions.CaPath); - } - catch (Exception ex) - { - logger.LogError("Root certificate import failed: {error}", - ex.Message); - throw; - } - finally - { - localTrustStore.Close(); - } + var authority = new X509Certificate2(amqpOptions.CaPath); + + // Configure the SSL settings + var sslOption = new SslOption + { + Enabled = true, + ServerName = amqpOptions.Host, + Certs = new X509Certificate2Collection(), + AcceptablePolicyErrors = SslPolicyErrors.RemoteCertificateChainErrors, + CertificateValidationCallback = (sender, + certificate, + chain, + sslPolicyErrors) => + { + if (sslPolicyErrors == SslPolicyErrors.None) + { + return true; + } + + if ((sslPolicyErrors & ~SslPolicyErrors.RemoteCertificateChainErrors) != 0) + { + logger.LogError("SSL validation failed: {errors}", + sslPolicyErrors); + return false; + } + + // If there is any error other than untrusted root or partial chain, fail the validation + if (chain!.ChainStatus.Any(status => status.Status is not X509ChainStatusFlags.UntrustedRoot and + not X509ChainStatusFlags.PartialChain)) + { + return false; + } + + // Disable some extensive checks that would fail on the authority that is not in store + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; + + // Add unknown authority to the store + chain.ChainPolicy.ExtraStore.Add(authority); + + // Check if the chain is valid for the actual server certificate (ie: trusted) + if (!chain.Build(new X509Certificate2(certificate!))) + { + logger.LogError("SSL chain validation failed."); + return false; + } + + // Check that the chain root is actually the specified authority (caCert) + var isTrusted = chain.ChainElements.Any(x => x.Certificate.Thumbprint == authority.Thumbprint); + + if (!isTrusted) + { + logger.LogError("Certificate chain root does not match the specified CA authority."); + } + + return isTrusted; + }, + }; + + // Apply the SSL settings to your RabbitMQ connection factory + var factory = new ConnectionFactory + { + HostName = amqpOptions.Host, + UserName = amqpOptions.User, + Password = amqpOptions.Password, + Ssl = sslOption, + }; + + serviceCollection.AddSingleton(factory); + } + else + { + logger.LogTrace("No CA path provided"); } serviceCollection.AddSingletonWithHealthCheck(nameof(IConnectionRabbit)); diff --git a/Adaptors/Redis/src/ServiceCollectionExt.cs b/Adaptors/Redis/src/ServiceCollectionExt.cs index 79c8ed094..44a949af3 100644 --- a/Adaptors/Redis/src/ServiceCollectionExt.cs +++ b/Adaptors/Redis/src/ServiceCollectionExt.cs @@ -15,8 +15,10 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -using System; using System.IO; +using System.Linq; +using System.Net.Security; +using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using ArmoniK.Api.Common.Utils; @@ -70,53 +72,88 @@ public static IServiceCollection AddRedis(this IServiceCollection serviceCollect if (!string.IsNullOrEmpty(redisOptions.CaPath)) { - var localTrustStore = new X509Store(StoreName.Root); - var certificateCollection = new X509Certificate2Collection(); - try + var authority = new X509Certificate2(redisOptions.CaPath); + + // Configure the SSL settings (https://stackexchange.github.io/StackExchange.Redis/Configuration.html) + var config = new ConfigurationOptions + { + ClientName = redisOptions.ClientName, + ReconnectRetryPolicy = new ExponentialRetry(10), + Ssl = redisOptions.Ssl, + AbortOnConnectFail = true, + SslHost = redisOptions.SslHost, + Password = redisOptions.Password, + User = redisOptions.User, + SslProtocols = SslProtocols.Tls12, + }; + config.CertificateValidation += (sender, + certificate, + chain, + sslPolicyErrors) => + { + if (sslPolicyErrors == SslPolicyErrors.None) + { + return true; + } + + if ((sslPolicyErrors & ~SslPolicyErrors.RemoteCertificateChainErrors) != 0) + { + logger.LogError("SSL validation failed: {SslPolicyErrors}", + sslPolicyErrors); + return false; + } + + // If there is any error other than untrusted root or partial chain, fail the validation + if (chain!.ChainStatus.Any(status => status.Status is not X509ChainStatusFlags.UntrustedRoot and + not X509ChainStatusFlags.PartialChain)) + { + return false; + } + + // Disable some extensive checks that would fail on the authority that is not in store + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; + + // Add unknown authority to the store + chain.ChainPolicy.ExtraStore.Add(authority); + + // Check if the chain is valid for the actual server certificate (ie: trusted) + if (!chain.Build(new X509Certificate2(certificate!))) + { + logger.LogError("SSL chain validation failed."); + return false; + } + + // Check that the chain root is actually the specified authority (caCert) + var isTrusted = chain.ChainElements.Any(x => x.Certificate.Thumbprint == authority.Thumbprint); + + if (!isTrusted) + { + logger.LogError("Certificate chain root does not match the specified CA authority."); + } + + return isTrusted; + }; + config.EndPoints.Add(redisOptions.EndpointUrl); + + if (redisOptions.Timeout > 0) { - certificateCollection.ImportFromPemFile(redisOptions.CaPath); - localTrustStore.Open(OpenFlags.ReadWrite); - localTrustStore.AddRange(certificateCollection); - logger.LogTrace("Imported Redis certificate from file {path}", - redisOptions.CaPath); + config.ConnectTimeout = redisOptions.Timeout; } - catch (Exception ex) - { - logger.LogError("Root certificate import failed: {error}", - ex.Message); - throw; - } - finally - { - localTrustStore.Close(); - } - } - var config = new ConfigurationOptions - { - ClientName = redisOptions.ClientName, - ReconnectRetryPolicy = new ExponentialRetry(10), - Ssl = redisOptions.Ssl, - AbortOnConnectFail = true, - SslHost = redisOptions.SslHost, - Password = redisOptions.Password, - User = redisOptions.User, - }; - config.EndPoints.Add(redisOptions.EndpointUrl); - - if (redisOptions.Timeout > 0) + logger.LogDebug("setup connection to Redis at {EndpointUrl} with user {user}", + redisOptions.EndpointUrl, + redisOptions.User); + + serviceCollection.AddSingleton(_ => ConnectionMultiplexer.Connect(config, + TextWriter.Null) + .GetDatabase()); + serviceCollection.AddSingletonWithHealthCheck(nameof(IObjectStorage)); + } + else { - config.ConnectTimeout = redisOptions.Timeout; + logger.LogTrace("No CA path provided"); } - - logger.LogDebug("setup connection to Redis at {EndpointUrl} with user {user}", - redisOptions.EndpointUrl, - redisOptions.User); - - serviceCollection.AddSingleton(_ => ConnectionMultiplexer.Connect(config, - TextWriter.Null) - .GetDatabase()); - serviceCollection.AddSingletonWithHealthCheck(nameof(IObjectStorage)); } return serviceCollection;