From 3952a93a8ad58bc19e11e681a938465df35da6c7 Mon Sep 17 00:00:00 2001 From: dckorben <32177800+dckorben@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:44:26 -0700 Subject: [PATCH 1/6] Fallback to System Cert Store --- QuickFIXn/Transport/SslStreamFactory.cs | 26 +++++++++++++++++ UnitTests/SslStreamFactoryTest.cs | 37 ++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/QuickFIXn/Transport/SslStreamFactory.cs b/QuickFIXn/Transport/SslStreamFactory.cs index c9d0cdbce..794782b6c 100644 --- a/QuickFIXn/Transport/SslStreamFactory.cs +++ b/QuickFIXn/Transport/SslStreamFactory.cs @@ -183,6 +183,32 @@ internal bool VerifyRemoteCertificate( // If CA Certificate is specified then validate against the CA certificate, otherwise it is validated against the installed certificates if (string.IsNullOrEmpty(_socketSettings.CACertificatePath)) { _nonSessionLog.Log(LogLevel.Warning, "CACertificatePath is not specified"); + + X509Chain chain = new(); + chain.ChainPolicy.RevocationMode = _socketSettings.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck; + + bool isValid = chain.Build((X509Certificate2)certificate); + if (isValid) + { + bool isChainValid = true; + foreach (var status in chain.ChainStatus) + { + if (!status.Status.HasFlag(X509ChainStatusFlags.NoError)) + { + isChainValid = false; + break; + } + } + if (isChainValid) + // resets the sslPolicyErrors.RemoteCertificateChainErrors status + sslPolicyErrors &= ~SslPolicyErrors.RemoteCertificateChainErrors; + else + sslPolicyErrors |= SslPolicyErrors.RemoteCertificateChainErrors; + } + else + { + sslPolicyErrors |= SslPolicyErrors.RemoteCertificateChainErrors; + } } else { diff --git a/UnitTests/SslStreamFactoryTest.cs b/UnitTests/SslStreamFactoryTest.cs index 6b89b382e..4d5374887 100644 --- a/UnitTests/SslStreamFactoryTest.cs +++ b/UnitTests/SslStreamFactoryTest.cs @@ -18,10 +18,18 @@ public class SslStreamFactoryTest const string ServerCertificatePath = "serverCertificate.cer"; const string ClientCertificatePath = "clientCertificate.cer"; + const string CaIntermediateCertificatePath = "CaIntermediateCertificate.cer"; + const string ServerIntermediateCertificatePath = "serverIntermediateCertificate.cer"; + const string ClientIntermediateCertificatePath = "clientIntermediateCertificate.cer"; + X509Certificate2 CaCertificate { get; set; } = null!; X509Certificate2 ClientCertificate { get; set; } = null!; X509Certificate2 ServerCertificate { get; set; } = null!; + X509Certificate2 CaIntermediateCertificate { get; set; } = null!; + X509Certificate2 ClientIntermediateCertificate { get; set; } = null!; + X509Certificate2 ServerIntermediateCertificate { get; set; } = null!; + [OneTimeSetUp] public void BuildCerts() { @@ -37,6 +45,20 @@ public void BuildCerts() File.WriteAllBytes(ClientCertificatePath, clientCertificate.Export(X509ContentType.Cert)); ClientCertificate = clientCertificate; + + var caIntermediateCertificate = CreateCACertificate(caCertificate); + File.WriteAllBytes(CaIntermediateCertificatePath, caIntermediateCertificate.Export(X509ContentType.Cert)); + CaIntermediateCertificate = caIntermediateCertificate; + + var serverIntermediateCertificate = CreateServerCertificate(caIntermediateCertificate); + File.WriteAllBytes(ServerIntermediateCertificatePath, serverIntermediateCertificate.Export(X509ContentType.Cert)); + ServerIntermediateCertificate = serverIntermediateCertificate; + + var clientIntermediateCertificate = CreateClientCertificate(caIntermediateCertificate); + File.WriteAllBytes(ClientIntermediateCertificatePath, clientIntermediateCertificate.Export(X509ContentType.Cert)); + ClientIntermediateCertificate = clientIntermediateCertificate; + + var differentCaCertificate = CreateCACertificate(); File.WriteAllBytes(DifferentCaCertificatePath, differentCaCertificate.Export(X509ContentType.Cert)); } @@ -58,10 +80,23 @@ static X509Certificate2 CreateCACertificate() request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true)); request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false)); - X509Certificate2 certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(5)); + X509Certificate2 certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(10)); return certificate; } + static X509Certificate2 CreateCACertificate(X509Certificate2 caCertificate) + { + var rsa = RSA.Create(); + var request = new CertificateRequest("CN=127.0.0.1 Test Intermediate CA", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + request.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true)); + request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false)); + + X509Certificate2 certificate = request.Create(caCertificate, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(5), [1, 0, 0, 0, 0, 0, 0, 0]); + + var rootCertificate = certificate.CopyWithPrivateKey(rsa); + return rootCertificate; + } + static X509Certificate2 CreateServerCertificate(X509Certificate2 caCertificate) { var rsa = RSA.Create(); From 11cd4aea5762997564e4bebc0605489b00f85d57 Mon Sep 17 00:00:00 2001 From: dckorben <32177800+dckorben@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:48:32 -0700 Subject: [PATCH 2/6] Update log mesage --- QuickFIXn/Transport/SslStreamFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QuickFIXn/Transport/SslStreamFactory.cs b/QuickFIXn/Transport/SslStreamFactory.cs index 794782b6c..965ebeb80 100644 --- a/QuickFIXn/Transport/SslStreamFactory.cs +++ b/QuickFIXn/Transport/SslStreamFactory.cs @@ -182,7 +182,7 @@ internal bool VerifyRemoteCertificate( // If CA Certificate is specified then validate against the CA certificate, otherwise it is validated against the installed certificates if (string.IsNullOrEmpty(_socketSettings.CACertificatePath)) { - _nonSessionLog.Log(LogLevel.Warning, "CACertificatePath is not specified"); + _nonSessionLog.Log(LogLevel.Warning, "CACertificatePath is not specified, using machine store"); X509Chain chain = new(); chain.ChainPolicy.RevocationMode = _socketSettings.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck; From 821c639fde815d41798ad80fd38c4eb7c7b8ef16 Mon Sep 17 00:00:00 2001 From: dckorben <32177800+dckorben@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:29:15 -0700 Subject: [PATCH 3/6] Log Invalid Certificate Status, Upgrade to Error Severity When Certificate Chain Fails --- QuickFIXn/Transport/SslStreamFactory.cs | 37 ++++++++++++++++++++----- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/QuickFIXn/Transport/SslStreamFactory.cs b/QuickFIXn/Transport/SslStreamFactory.cs index 965ebeb80..fd5e93ad4 100644 --- a/QuickFIXn/Transport/SslStreamFactory.cs +++ b/QuickFIXn/Transport/SslStreamFactory.cs @@ -1,12 +1,13 @@ +using Microsoft.Extensions.Logging; +using QuickFix.Logger; +using QuickFix.Util; using System; +using System.Data; using System.Diagnostics; using System.IO; using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; -using Microsoft.Extensions.Logging; -using QuickFix.Logger; -using QuickFix.Util; namespace QuickFix.Transport; @@ -174,7 +175,7 @@ internal bool VerifyRemoteCertificate( // Validate enhanced key usage if (!ContainsEnhancedKeyUsage(certificate, enhancedKeyUsage)) { var role = enhancedKeyUsage.Equals(CLIENT_AUTHENTICATION_OID, StringComparison.Ordinal) ? "client" : "server"; - _nonSessionLog.Log(LogLevel.Warning, + _nonSessionLog.Log(LogLevel.Error, "Remote certificate is not intended for {Role} authentication: It is missing enhanced key usage {KeyUsage}", role, enhancedKeyUsage); return false; @@ -182,7 +183,7 @@ internal bool VerifyRemoteCertificate( // If CA Certificate is specified then validate against the CA certificate, otherwise it is validated against the installed certificates if (string.IsNullOrEmpty(_socketSettings.CACertificatePath)) { - _nonSessionLog.Log(LogLevel.Warning, "CACertificatePath is not specified, using machine store"); + _nonSessionLog.Log(LogLevel.Information, "CACertificatePath is not specified, using local trust store"); X509Chain chain = new(); chain.ChainPolicy.RevocationMode = _socketSettings.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck; @@ -195,6 +196,10 @@ internal bool VerifyRemoteCertificate( { if (!status.Status.HasFlag(X509ChainStatusFlags.NoError)) { + _nonSessionLog.Log(LogLevel.Warning, + "Certificate Chain: {Status} {StatusInformation}", + status.Status, status.StatusInformation); + isChainValid = false; break; } @@ -207,6 +212,13 @@ internal bool VerifyRemoteCertificate( } else { + foreach (var status in chain.ChainStatus) + { + _nonSessionLog.Log(LogLevel.Error, + "Certificate Chain Build Failure: {Status} {StatusInformation}", + status.Status, status.StatusInformation); + } + sslPolicyErrors |= SslPolicyErrors.RemoteCertificateChainErrors; } } @@ -217,7 +229,7 @@ internal bool VerifyRemoteCertificate( X509Certificate2? caCert = SslCertCache.LoadCertificate(caCertPath, null); if (caCert is null) { - _nonSessionLog.Log(LogLevel.Warning, + _nonSessionLog.Log(LogLevel.Error, "Certificate '{CertificatePath}' could not be loaded from store or path '{Directory}'", caCertPath, Directory.GetCurrentDirectory()); return false; @@ -235,6 +247,10 @@ internal bool VerifyRemoteCertificate( foreach (var status in chain.ChainStatus) { if (!status.Status.HasFlag(X509ChainStatusFlags.NoError)) { + _nonSessionLog.Log(LogLevel.Warning, + "Certificate Chain: {Status} {StatusInformation}", + status.Status, status.StatusInformation); + isChainValid = false; break; } @@ -246,6 +262,13 @@ internal bool VerifyRemoteCertificate( sslPolicyErrors |= SslPolicyErrors.RemoteCertificateChainErrors; } else { + foreach (var status in chain.ChainStatus) + { + _nonSessionLog.Log(LogLevel.Error, + "Certificate Chain Build Failure: {Status} {StatusInformation}", + status.Status, status.StatusInformation); + } + sslPolicyErrors |= SslPolicyErrors.RemoteCertificateChainErrors; } } @@ -253,7 +276,7 @@ internal bool VerifyRemoteCertificate( // Any basic authentication check failed, do after checking CA if (sslPolicyErrors != SslPolicyErrors.None) { - _nonSessionLog.Log(LogLevel.Warning, + _nonSessionLog.Log(LogLevel.Error, "Remote certificate was not recognized as a valid certificate: {Errors}", sslPolicyErrors); return false; } From d3e036ade76bf17e4440c75045def8af2797a85c Mon Sep 17 00:00:00 2001 From: dckorben <32177800+dckorben@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:32:09 -0700 Subject: [PATCH 4/6] Remove erroneously added namespace --- QuickFIXn/Transport/SslStreamFactory.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/QuickFIXn/Transport/SslStreamFactory.cs b/QuickFIXn/Transport/SslStreamFactory.cs index fd5e93ad4..2da2e0e3f 100644 --- a/QuickFIXn/Transport/SslStreamFactory.cs +++ b/QuickFIXn/Transport/SslStreamFactory.cs @@ -2,7 +2,6 @@ using QuickFix.Logger; using QuickFix.Util; using System; -using System.Data; using System.Diagnostics; using System.IO; using System.Net.Security; From 3266bffc31e1529270f2fe1e9f8ad86003f349c3 Mon Sep 17 00:00:00 2001 From: dckorben <32177800+dckorben@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:33:53 -0700 Subject: [PATCH 5/6] Remove Reordered Namespaces --- QuickFIXn/Transport/SslStreamFactory.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/QuickFIXn/Transport/SslStreamFactory.cs b/QuickFIXn/Transport/SslStreamFactory.cs index 2da2e0e3f..8feaf9b11 100644 --- a/QuickFIXn/Transport/SslStreamFactory.cs +++ b/QuickFIXn/Transport/SslStreamFactory.cs @@ -1,12 +1,12 @@ -using Microsoft.Extensions.Logging; -using QuickFix.Logger; -using QuickFix.Util; using System; using System.Diagnostics; using System.IO; using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Logging; +using QuickFix.Logger; +using QuickFix.Util; namespace QuickFix.Transport; From 64f77e960a2ea6fc624826becfde840d85f995e4 Mon Sep 17 00:00:00 2001 From: dckorben <32177800+dckorben@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:41:52 -0700 Subject: [PATCH 6/6] Lint --- QuickFIXn/Transport/SslStreamFactory.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/QuickFIXn/Transport/SslStreamFactory.cs b/QuickFIXn/Transport/SslStreamFactory.cs index 8feaf9b11..7e854316f 100644 --- a/QuickFIXn/Transport/SslStreamFactory.cs +++ b/QuickFIXn/Transport/SslStreamFactory.cs @@ -209,8 +209,7 @@ internal bool VerifyRemoteCertificate( else sslPolicyErrors |= SslPolicyErrors.RemoteCertificateChainErrors; } - else - { + else { foreach (var status in chain.ChainStatus) { _nonSessionLog.Log(LogLevel.Error,