diff --git a/QuickFIXn/Transport/SslStreamFactory.cs b/QuickFIXn/Transport/SslStreamFactory.cs index c9d0cdbce..7e854316f 100644 --- a/QuickFIXn/Transport/SslStreamFactory.cs +++ b/QuickFIXn/Transport/SslStreamFactory.cs @@ -174,7 +174,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 +182,43 @@ 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.Information, "CACertificatePath is not specified, using local trust store"); + + 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)) + { + _nonSessionLog.Log(LogLevel.Warning, + "Certificate Chain: {Status} {StatusInformation}", + status.Status, status.StatusInformation); + + isChainValid = false; + break; + } + } + if (isChainValid) + // resets the sslPolicyErrors.RemoteCertificateChainErrors status + sslPolicyErrors &= ~SslPolicyErrors.RemoteCertificateChainErrors; + else + 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; + } } else { @@ -191,7 +227,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; @@ -209,6 +245,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; } @@ -220,6 +260,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; } } @@ -227,7 +274,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; } 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();