Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 51 additions & 4 deletions QuickFIXn/Transport/SslStreamFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,15 +174,51 @@ 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;
}

// If CA Certificate is specified then validate against the CA certificate, otherwise it is validated against the installed certificates
Copy link
Copy Markdown
Contributor Author

@dckorben dckorben Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notable that this comment SAYS it validates against installed certificates but that is actually not true. It validates against installed certificates if it is a HTTPS/TLS connection, which was probably the intended meaning. If you specify a certificate but not a CA, it just fails. In the case of #990, you can specify a certificate from a Public CA but then validation fails and because many CAs use a lineage of certificates you cannot validate the chain with the existing configuration options.

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
{
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -220,14 +260,21 @@ 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;
}
}

// 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;
}
Expand Down
37 changes: 36 additions & 1 deletion UnitTests/SslStreamFactoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand All @@ -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));
}
Expand All @@ -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();
Expand Down