Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# ChangeLog - DataStax C# Driver

## 3.22.1

2026-08-26

### Improvements

* [[#634](https://github.com/datastax/csharp-driver/pull/634)] Improvements to Astra cert validator

## 3.22.0

2024-09-30
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//
//
// Copyright (C) DataStax Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -12,10 +12,11 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//

using System;
using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

using Cassandra.DataStax.Cloud;
Expand All @@ -27,6 +28,14 @@ namespace Cassandra.Tests.DataStax.Cloud
[TestFixture]
public class CustomCaCertificateValidatorTests
{
// rootcacsharp.crt — self-signed CA used across multiple tests
private const string RootCaB64 =
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvakNDQWVZQ0NRQ01JNHlFM0J5WnpUQU5CZ2txaGtpRzl3MEJBUXNGQURCQU1Rc3dDUVlEVlFRR0V3SlYKVXpFUk1BOEdBMVVFQ2d3SVJHRjBZVk4wWVhneERqQU1CZ05WQkFzTUJVTnNiM1ZrTVE0d0RBWURWUVFEREFWRApiRzkxWkRBZ0Z3MHlNakExTXpBeE1qTTJNRGhhR0E4ek1ESXhNRGt6TURFeU16WXdPRm93UURFTE1Ba0dBMVVFCkJoTUNWVk14RVRBUEJnTlZCQW9NQ0VSaGRHRlRkR0Y0TVE0d0RBWURWUVFMREFWRGJHOTFaREVPTUF3R0ExVUUKQXd3RlEyeHZkV1F3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRGFZTjVBcm5VOQo5b2Q5cXdPMzVaRlBwdFpCc0psc29CQWl5b0V1WEpCbFFwdnlveHY2ckFXYkJaMkxpWEFvZkZWdzVjWThxTlNxCmUzRytjWmJzUm0xbk9Yc0lTRnVYekhGSWFJWWEzZi9OblIzc25SRG1uZUwwS3lhUVI5VnFvMCt5V0RUMmFlZWsKSDNrNFdrUlJrMEZtOUhIUWlKTWFTdU05WC9nQnhyUTdiMkxBOFRjM2FjTForbmYxdGpSa01ZN0hDNnJ6TW9rRwp0QmhCN3lxL1dtMzFOVk1ucVk2UnZhOWpBV1lIcW1YWXZkOG9uMmRsTDlzVzEybFRUNHd4Qkp1VTV6Mzd5bDhXClZOY0RkY3lhdWJzQUtDU3NwZmord2pVL0ZEL00zZUdYNEdZQVlDdjdQbStNL2NMNG1wMDRtR3dEakhVOE1RS28KUi91QmRRbllRN2lYQWdNQkFBRXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBTFhvL01UUUVYY29vQ3dTSVczaAovemhaSFNrUko1SmYyd2pjTTlPTmxEaVI1K01NU2pYVFVTMHF6amlhZFNvSzlZeGdWN1ByVHpjMWR0cVNzTjdXCllQNHM0VG9zbWRzRVRTdmVwRXFRSklLRHZyVFJaTDlPV2hLb1BVRTd3NzJVVnVYcERaWHIraTNNQ3p3U05zZGwKNzFXbHpVcXJqcGJCSkZnU21xZmZkNmJuVXExWVhKU3orcy9KSnV4SmlrNGVqTGlIbTh3MWhIckUvYWRvYUtMbQo1elYxaHpPT3Y0d0VnSTQ3VnNvMUxYenQzQmROeXVVMnZPR1R4Wmwwa0E1bUVSRUwxR3J0TEova251SkF6aWZ5ClFKMnVlU2JMTVZadXBsNy84MW5Ib3k3eVNaKzNYM1VUUkdQNzNNYWM5cTdvK0ZJeTV2M0laL3NJM0FEaWM4NXUKWGVvPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==";

// localhostcsharp.crt — leaf cert signed by RootCaB64
private const string LocalhostCertB64 =
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURBakNDQWVvQ0NRQ2ZCNGxlaUM0ZGl6QU5CZ2txaGtpRzl3MEJBUXNGQURCQU1Rc3dDUVlEVlFRR0V3SlYKVXpFUk1BOEdBMVVFQ2d3SVJHRjBZVk4wWVhneERqQU1CZ05WQkFzTUJVTnNiM1ZrTVE0d0RBWURWUVFEREFWRApiRzkxWkRBZ0Z3MHlNakExTXpBeE1qUXpOVE5hR0E4ek1ESXhNRGt6TURFeU5ETTFNMW93UkRFTE1Ba0dBMVVFCkJoTUNWVk14RVRBUEJnTlZCQW9UQ0VSaGRHRlRkR0Y0TVE0d0RBWURWUVFMRXdWRGJHOTFaREVTTUJBR0ExVUUKQXhNSmJHOWpZV3hvYjNOME1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2Z3TwpSNW5CQW9PaWhsengrTSt3enhCdzg2OHU5L3FxZlVyRlIycXlySFBYZkNjSm14ZUFXVE5UMXllbWcyd2pZSUZmClZoU2VmOXZNSWlQbWh1eXd2R1RkVFlCcFdsZTNKRlY3ejdvb1JYb1d4SGwwNFJTNVY3Q0p6Sk1vNEdNRWZYamgKL2VyNVk2NU9ibnY3Z2hiN0IzNEh5NDVzZFMyWWpPRklZVFFzZXgwMDhIMnpDSmVoT3J0OWFZWlFOVEp1Zk9BMwovNnJRaG9hZVhjKy9DdWdKUWlkOEQyRlJvUENmM1NsQlkzeHFZbmhJNGJmQWJvUTJqYmJINitzYjlYRDFsdTVCCiswTWl5NGcwL00yQTA0NWZpYnJjQVAvRG9YWnpSdkd5SWlmYU55RHltQjZERWhtUFNNUkNZRWdpWlFlMmpkRVQKSUVEMkk5NnJBZjhpb3B2ZG13SURBUUFCTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBM2V3MVhlaG9WMHpBZAo2SkJuS1FzMkdkVjl5WFVFRVhEQTQ3M0xJZ05JWWFrVEpWbVZTVGdQUlNEbW1LRzkyNlUwUUgzREtwbitsTDNxCnQ4V1IyM3gwRzJSeXFtaGJ5MTFlUXFkWUZrRzNIOGxVMVVIZTM3LzI3Q29EeE80cnpNUFNJd0JWRVN3aTVsbHoKc1FHWXY2K1dnZUFXUHpRYzlCOVV5UFYyenFSbitCQTNOdDBPSnJOdG1UcFJsRE5XU2NoZ1VhV1BCdXhRVk1kNgo2OEM5dkczNk9hYnJFZDBmakdJUWVaYlJjcHFPQVVEbEpMaUY3SElEU0NkNFo0MUxOM3pNdmMxTnhMN1orUDdhCm5jU2RTcjlkR0tIOXRvRm4ydk5Nd0JOZWgvbm5Xc3BRd3djSm85Yk9pZjNmeXlGOWZYWGF0TjVxM2dJcUhVd08KVXlFK1l1UjEKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=";

/// <summary>
/// Simulate classic Astra
/// </summary>
Expand All @@ -50,6 +59,79 @@ public void TestCertificateWithoutWildcardAndWithRootCaInChain()
Assert.True(new CustomCaCertificateValidator(ca, "localhost").Validate(cert, chain, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch));
}

/// <summary>
/// When the OS reports SslPolicyErrors.None (server cert is trusted by the OS
/// store) and the chain root thumbprint matches the bundle CA, the connection must be accepted.
/// </summary>
[Test]
public void SslPolicyErrors_None_WithMatchingBundleCa_ShouldAccept()
{
var ca = new X509Certificate2(Convert.FromBase64String(RootCaB64));
var cert = new X509Certificate2(Convert.FromBase64String(LocalhostCertB64));
var chain = new X509Chain();
chain.ChainPolicy.ExtraStore.Add(ca);
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.Build(cert);

Assert.True(
new CustomCaCertificateValidator(ca, "localhost")
.Validate(cert, chain, SslPolicyErrors.None),
"Should accept: OS says None and root thumbprint matches the bundle CA.");
}

#if !NET462
/// <summary>
/// When the OS reports SslPolicyErrors.None but the chain root does NOT match
/// the bundle CA (rogue OS-trusted CA), the connection must be rejected.
/// </summary>
[Test]
public void SslPolicyErrors_None_WithMismatchedBundleCa_ShouldReject()
{
var cert = new X509Certificate2(Convert.FromBase64String(LocalhostCertB64));

// A second self-signed CA — represents a rogue CA trusted by the OS but not in the bundle.
var rogueCa = CreateSelfSignedCa("CN=RogueCA");

// Chain was built against the real bundle CA (OS accepted it), but the validator
// is seeded with rogueCa — thumbprint will not match.
var bundleCa = new X509Certificate2(Convert.FromBase64String(RootCaB64));
var chain = new X509Chain();
chain.ChainPolicy.ExtraStore.Add(bundleCa);
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.Build(cert);

Assert.False(
new CustomCaCertificateValidator(rogueCa, "localhost")
.Validate(cert, chain, SslPolicyErrors.None),
"Should reject: OS says None but root thumbprint does NOT match the bundle CA.");
}

/// <summary>
/// When chain.Build() returns true (no chain errors) but the root thumbprint
/// does not match the bundle CA, the connection must be rejected.
/// </summary>
[Test]
public void ChainBuildSucceeds_WithMismatchedBundleCa_ShouldReject()
{
var cert = new X509Certificate2(Convert.FromBase64String(LocalhostCertB64));
var rogueCa = CreateSelfSignedCa("CN=RogueCA");

var bundleCa = new X509Certificate2(Convert.FromBase64String(RootCaB64));
var chain = new X509Chain();
chain.ChainPolicy.ExtraStore.Add(bundleCa);
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.Build(cert);

// Pass RemoteCertificateChainErrors so the chain block is entered;
// rogueCa thumbprint won't match the chain root regardless of which
// branch chain.Build() takes internally.
Assert.False(
new CustomCaCertificateValidator(rogueCa, "localhost")
.Validate(cert, chain, SslPolicyErrors.RemoteCertificateChainErrors),
"Should reject: chain root thumbprint does not match the bundle CA even when Build() returns true.");
}
#endif // !NET462

/// <summary>
/// Simulate serverless Astra
/// </summary>
Expand Down Expand Up @@ -165,5 +247,29 @@ public void TestCertificateWithoutCustomDomainInSan_WithCustomDomain()
Assert.AreEqual("*.example.com", cert.GetNameInfo(X509NameType.SimpleName, false));
Assert.False(new CustomCaCertificateValidator(cert, "test123.customdomain.com").Validate(cert, chain, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch));
}
// ── Helper ────────────────────────────────────────────────────────────────

#if !NET462
/// <summary>
/// Creates an in-memory self-signed CA certificate. Used to synthesise a rogue CA
/// whose thumbprint differs from the bundle CA.
/// </summary>
private static X509Certificate2 CreateSelfSignedCa(string subjectDn)
{
using (var rsa = RSA.Create(2048))
{
var req = new CertificateRequest(
subjectDn, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
req.CertificateExtensions.Add(
new X509BasicConstraintsExtension(
certificateAuthority: true, hasPathLengthConstraint: false,
pathLengthConstraint: 0, critical: true));
var cert = req.CreateSelfSigned(
DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(10));
// Export/re-import to detach the private key
return new X509Certificate2(cert.Export(X509ContentType.Cert));
}
}
#endif // !NET462
}
}
4 changes: 2 additions & 2 deletions src/Cassandra/Cassandra.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<Copyright>Copyright © by DataStax</Copyright>
<AssemblyTitle>DataStax C# Driver for Apache Cassandra</AssemblyTitle>
<AssemblyVersion>3.99.0.0</AssemblyVersion>
<FileVersion>3.22.0.0</FileVersion>
<VersionPrefix>3.22.0</VersionPrefix>
<FileVersion>3.22.1.0</FileVersion>
<VersionPrefix>3.22.1</VersionPrefix>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Authors>DataStax</Authors>
<TargetFrameworks Condition="'$(BuildCoreOnly)' != 'True'">net452;netstandard2.0</TargetFrameworks>
Expand Down
70 changes: 50 additions & 20 deletions src/Cassandra/DataStax/Cloud/CustomCACertificateValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,9 @@ public CustomCaCertificateValidator(X509Certificate2 trustedRootCertificateAutho

public bool Validate(X509Certificate cert, X509Chain chain, SslPolicyErrors errors)
{
if (errors == SslPolicyErrors.None)
{
return true;
}
// Do NOT short-circuit on SslPolicyErrors.None.
// Always rebuild the chain using the bundle CA and verify the root
// thumbprint regardless of what the OS-level validation decided.

X509Certificate2 cert2 = null;
var valid = true;
Expand Down Expand Up @@ -128,7 +127,11 @@ public bool Validate(X509Certificate cert, X509Chain chain, SslPolicyErrors erro

valid = validName;
}
if (valid && (errors & SslPolicyErrors.RemoteCertificateChainErrors) != 0)

// Always rebuild the chain against the bundle CA and verify the root thumbprint,
// regardless of whether the OS reported chain errors or None. This ensures the
// OS trust store is never the sole authority for accepting a connection.
if (valid)
{
var oldChain = chain;
chain = new X509Chain();
Expand All @@ -149,29 +152,56 @@ public bool Validate(X509Certificate cert, X509Chain chain, SslPolicyErrors erro
// clone CA object because on Mono it gets reset for some reason after using it to build a new chain
var clonedCa = new X509Certificate2(_trustedRootCertificateAuthority);
chain.ChainPolicy.ExtraStore.Add(clonedCa);

GetOrCreateCert2(ref cert2, cert);
if (!chain.Build(cert2))
{
// verify if the chain is correct
foreach (var status in chain.ChainStatus)
// chain.Build() returned false — inspect each status flag.
// UntrustedRoot is acceptable: the bundle CA is self-signed and will not be
// in the OS trust store on most machines.
if (chain.ChainStatus.Length == 0)
{
if (status.Status == X509ChainStatusFlags.NoError || status.Status == X509ChainStatusFlags.UntrustedRoot)
{
//Acceptable Status
}
else
// No status information means we cannot determine why the build failed.
// Treat as a hard failure rather than silently falling through to the
// thumbprint check with an unknown chain state.
CustomCaCertificateValidator.Logger.Error(
"SSL validation failed: chain.Build() returned false with no status information.");
valid = false;
}
else
{
foreach (var status in chain.ChainStatus)
{
CustomCaCertificateValidator.Logger.Error(
"Certificate chain validation failed. Found chain status {0} ({1}).", status.Status, status.StatusInformation);
valid = false;
break;
if (status.Status == X509ChainStatusFlags.NoError || status.Status == X509ChainStatusFlags.UntrustedRoot)
{
//Acceptable Status
}
else
{
CustomCaCertificateValidator.Logger.Error(
"Certificate chain validation failed. Found chain status {0} ({1}).", status.Status, status.StatusInformation);
valid = false;
break;
}
}
}
}

if (valid)
// Always verify the root thumbprint regardless of whether chain.Build() succeeded
// or failed with only UntrustedRoot.
if (valid)
{
if (chain.ChainElements.Count == 0)
{
// Should not happen after a successful or UntrustedRoot-only build, but
// guard against it explicitly to avoid an unhandled exception on the
// indexer that would produce an opaque SSL error with no log message.
CustomCaCertificateValidator.Logger.Error(
"SSL validation failed: certificate chain contains no elements.");
valid = false;
}
else
{
//Now that we have tested to see if the cert builds properly, we now will check if the thumbprint of the root ca matches our trusted one
var rootCertThumbprint = chain.ChainElements[chain.ChainElements.Count - 1].Certificate.Thumbprint;
if (rootCertThumbprint != _trustedRootCertificateAuthority.Thumbprint)
{
Expand All @@ -180,8 +210,8 @@ public bool Validate(X509Certificate cert, X509Chain chain, SslPolicyErrors erro
valid = false;
}
}

}

DisposeCert2(clonedCa);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<Description>This package builds on the DataStax Enterprise C# driver and DataStax C# Driver for Apache Cassandra, adding a metrics provider implementation using App Metrics.</Description>
<Copyright>Copyright © by DataStax</Copyright>
<AssemblyVersion>3.99.0.0</AssemblyVersion>
<FileVersion>3.22.0.0</FileVersion>
<VersionPrefix>3.22.0</VersionPrefix>
<FileVersion>3.22.1.0</FileVersion>
<VersionPrefix>3.22.1</VersionPrefix>
<Authors>DataStax</Authors>
<TargetFrameworks Condition="'$(BuildCoreOnly)' != 'True'">netstandard2.0;net461</TargetFrameworks>
<TargetFrameworks Condition="'$(BuildCoreOnly)' == 'True'">netstandard2.0</TargetFrameworks>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>This package builds on the DataStax Enterprise C# driver and DataStax C# Driver for Apache Cassandra, adding OpenTelemetry instrumentation.</Description>
<Copyright>Copyright © by DataStax</Copyright>
<AssemblyVersion>3.99.0.0</AssemblyVersion>
<FileVersion>3.22.0.0</FileVersion>
<VersionPrefix>3.22.0</VersionPrefix>
<FileVersion>3.22.1.0</FileVersion>
<VersionPrefix>3.22.1</VersionPrefix>
<Authors>DataStax</Authors>
<TargetFramework>netstandard2.0</TargetFramework>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsNotAsErrors>NU1901;NU1902;NU1903;NU1904</WarningsNotAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AssemblyName>Cassandra.OpenTelemetry</AssemblyName>
<Product>DataStax C# Drivers OpenTelemetry Extension</Product>
Expand Down