This is an automated email from the ASF dual-hosted git repository.

curth pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-adbc.git


The following commit(s) were added to refs/heads/main by this push:
     new 73890b694 feat(csharp/src/Drivers/Apache): Implement self signed ssl 
certificate validation for Spark, Impala & Hive (#3224)
73890b694 is described below

commit 73890b694dfe94369681cfb40c3e22cf5f6ee0d9
Author: Sudhir Reddy Emmadi <reddysudhi...@gmail.com>
AuthorDate: Tue Aug 5 23:11:13 2025 +0530

    feat(csharp/src/Drivers/Apache): Implement self signed ssl certificate 
validation for Spark, Impala & Hive (#3224)
    
    Co-authored-by: Sudhir Emmadi <emmadisud...@microsoft.com>
---
 .../Apache/Hive2/HiveServer2StandardConnection.cs  |  3 +-
 .../src/Drivers/Apache/Hive2/HiveServer2TlsImpl.cs | 96 ++++++++++++----------
 .../Apache/Impala/ImpalaStandardConnection.cs      |  6 +-
 .../Drivers/Apache/Hive2/HiveServer2TlsImplTest.cs |  4 -
 4 files changed, 60 insertions(+), 49 deletions(-)

diff --git a/csharp/src/Drivers/Apache/Hive2/HiveServer2StandardConnection.cs 
b/csharp/src/Drivers/Apache/Hive2/HiveServer2StandardConnection.cs
index 66f175a14..4f89a3904 100644
--- a/csharp/src/Drivers/Apache/Hive2/HiveServer2StandardConnection.cs
+++ b/csharp/src/Drivers/Apache/Hive2/HiveServer2StandardConnection.cs
@@ -18,6 +18,7 @@
 using System;
 using System.Collections.Generic;
 using System.Net;
+using System.Net.Security;
 using System.Security.Cryptography.X509Certificates;
 using System.Threading;
 using System.Threading.Tasks;
@@ -115,7 +116,7 @@ namespace Apache.Arrow.Adbc.Drivers.Apache.Hive2
                     ? new X509Certificate2(TlsOptions.TrustedCertificatePath!)
                     : null;
 
-                var certValidator = 
HiveServer2TlsImpl.GetCertificateValidator(TlsOptions);
+                RemoteCertificateValidationCallback certValidator = (sender, 
cert, chain, errors) => HiveServer2TlsImpl.ValidateCertificate(cert, errors, 
TlsOptions);
 
                 if (IPAddress.TryParse(hostName!, out var ipAddress))
                 {
diff --git a/csharp/src/Drivers/Apache/Hive2/HiveServer2TlsImpl.cs 
b/csharp/src/Drivers/Apache/Hive2/HiveServer2TlsImpl.cs
index 345974d77..9a9311d7a 100644
--- a/csharp/src/Drivers/Apache/Hive2/HiveServer2TlsImpl.cs
+++ b/csharp/src/Drivers/Apache/Hive2/HiveServer2TlsImpl.cs
@@ -65,11 +65,8 @@ namespace Apache.Arrow.Adbc.Drivers.Apache.Hive2
             tlsProperties.DisableServerCertificateValidation = false;
             tlsProperties.AllowHostnameMismatch = 
properties.TryGetValue(HttpTlsOptions.AllowHostnameMismatch, out string? 
allowHostnameMismatch) && bool.TryParse(allowHostnameMismatch, out bool 
allowHostnameMismatchBool) && allowHostnameMismatchBool;
             tlsProperties.AllowSelfSigned = 
properties.TryGetValue(HttpTlsOptions.AllowSelfSigned, out string? 
allowSelfSigned) && bool.TryParse(allowSelfSigned, out bool 
allowSelfSignedBool) && allowSelfSignedBool;
-            if (tlsProperties.AllowSelfSigned)
-            {
-                if 
(!properties.TryGetValue(HttpTlsOptions.TrustedCertificatePath, out string? 
trustedCertificatePath)) return tlsProperties;
-                tlsProperties.TrustedCertificatePath = trustedCertificatePath 
!= "" && File.Exists(trustedCertificatePath) ? trustedCertificatePath : throw 
new FileNotFoundException("Trusted certificate path is invalid or file does not 
exist.");
-            }
+            if (!properties.TryGetValue(HttpTlsOptions.TrustedCertificatePath, 
out string? trustedCertificatePath)) return tlsProperties;
+            tlsProperties.TrustedCertificatePath = trustedCertificatePath != 
"" && File.Exists(trustedCertificatePath) ? trustedCertificatePath : throw new 
FileNotFoundException("Trusted certificate path is invalid or file does not 
exist.");
             return tlsProperties;
         }
 
@@ -78,33 +75,39 @@ namespace Apache.Arrow.Adbc.Drivers.Apache.Hive2
             HttpClientHandler httpClientHandler = new();
             if (tlsProperties.IsTlsEnabled)
             {
-                httpClientHandler.ServerCertificateCustomValidationCallback = 
(request, certificate, chain, policyErrors) =>
-                {
-                    if (policyErrors == SslPolicyErrors.None || 
tlsProperties.DisableServerCertificateValidation) return true;
-                    if 
(string.IsNullOrEmpty(tlsProperties.TrustedCertificatePath))
-                    {
-                        return
-                            
(!policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors) || 
tlsProperties.AllowSelfSigned)
-                        && 
(!policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch) || 
tlsProperties.AllowHostnameMismatch);
-                    }
-                    if (certificate == null)
-                        return false;
-                    X509Certificate2 customCertificate = new 
X509Certificate2(tlsProperties.TrustedCertificatePath);
-                    X509Chain chain2 = new X509Chain();
-                    chain2.ChainPolicy.ExtraStore.Add(customCertificate);
-
-                    // "tell the X509Chain class that I do trust this root 
certs and it should check just the certs in the chain and nothing else"
-                    chain2.ChainPolicy.VerificationFlags = 
X509VerificationFlags.AllowUnknownCertificateAuthority;
-
-                    // Build the chain and verify
-                    return chain2.Build(certificate);
-                };
+                httpClientHandler.ServerCertificateCustomValidationCallback = 
(request, cert, chain, errors) => ValidateCertificate(cert, errors, 
tlsProperties);
             }
             proxyConfigurator.ConfigureProxy(httpClientHandler);
             httpClientHandler.AutomaticDecompression = 
DecompressionMethods.GZip | DecompressionMethods.Deflate;
             return httpClientHandler;
         }
 
+        static private bool IsSelfSigned(X509Certificate2 cert)
+        {
+            return cert.Subject == cert.Issuer && IsSignedBy(cert, cert);
+        }
+
+        static private bool IsSignedBy(X509Certificate2 cert, X509Certificate2 
issuer)
+        {
+            try
+            {
+                using (var chain = new X509Chain())
+                {
+                    chain.ChainPolicy.ExtraStore.Add(issuer);
+                    chain.ChainPolicy.VerificationFlags = 
X509VerificationFlags.AllowUnknownCertificateAuthority;
+                    chain.ChainPolicy.RevocationMode = 
X509RevocationMode.Online;
+
+                    return chain.Build(cert)
+                        && chain.ChainElements.Count == 1
+                        && chain.ChainElements[0].Certificate.Thumbprint == 
issuer.Thumbprint;
+                }
+            }
+            catch
+            {
+                return false;
+            }
+        }
+
         static internal TlsProperties 
GetStandardTlsOptions(IReadOnlyDictionary<string, string> properties)
         {
             TlsProperties tlsProperties = new();
@@ -130,28 +133,37 @@ namespace Apache.Arrow.Adbc.Drivers.Apache.Hive2
             tlsProperties.DisableServerCertificateValidation = false;
             tlsProperties.AllowHostnameMismatch = 
properties.TryGetValue(StandardTlsOptions.AllowHostnameMismatch, out string? 
allowHostnameMismatch) && bool.TryParse(allowHostnameMismatch, out bool 
allowHostnameMismatchBool) && allowHostnameMismatchBool;
             tlsProperties.AllowSelfSigned = 
properties.TryGetValue(StandardTlsOptions.AllowSelfSigned, out string? 
allowSelfSigned) && bool.TryParse(allowSelfSigned, out bool 
allowSelfSignedBool) && allowSelfSignedBool;
-            if (tlsProperties.AllowSelfSigned)
-            {
-                if 
(!properties.TryGetValue(StandardTlsOptions.TrustedCertificatePath, out string? 
trustedCertificatePath)) return tlsProperties;
-                tlsProperties.TrustedCertificatePath = trustedCertificatePath 
!= "" && File.Exists(trustedCertificatePath) ? trustedCertificatePath : throw 
new FileNotFoundException("Trusted certificate path is invalid or file does not 
exist.");
-            }
+            if 
(!properties.TryGetValue(StandardTlsOptions.TrustedCertificatePath, out string? 
trustedCertificatePath)) return tlsProperties;
+            tlsProperties.TrustedCertificatePath = trustedCertificatePath != 
"" && File.Exists(trustedCertificatePath) ? trustedCertificatePath : throw new 
FileNotFoundException("Trusted certificate path is invalid or file does not 
exist.");
             return tlsProperties;
         }
 
-        static internal RemoteCertificateValidationCallback 
GetCertificateValidator(TlsProperties tlsProperties)
+        static internal bool ValidateCertificate(X509Certificate? cert, 
SslPolicyErrors policyErrors, TlsProperties tlsProperties)
         {
-            return (object sender, X509Certificate? certificate, X509Chain? 
chain, SslPolicyErrors policyErrors) =>
-            {
-                if (policyErrors == SslPolicyErrors.None || 
tlsProperties.DisableServerCertificateValidation) return true;
-                if (string.IsNullOrEmpty(tlsProperties.TrustedCertificatePath))
-                {
-                    return
-                        
(!policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors) || 
tlsProperties.AllowSelfSigned)
-                        && 
(!policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch) || 
tlsProperties.AllowHostnameMismatch);
-                }
+            if (policyErrors == SslPolicyErrors.None || 
tlsProperties.DisableServerCertificateValidation)
+                return true;
 
+            if (cert == null || !(cert is X509Certificate2 cert2))
                 return false;
-            };
+
+            bool isNameMismatchError = 
policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch) && 
!tlsProperties.AllowHostnameMismatch;
+
+            if (isNameMismatchError) return false;
+
+            if (string.IsNullOrEmpty(tlsProperties.TrustedCertificatePath))
+            {
+                return 
!policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors) || 
(tlsProperties.AllowSelfSigned && IsSelfSigned(cert2));
+            }
+
+            X509Certificate2 trustedRoot = new 
X509Certificate2(tlsProperties.TrustedCertificatePath);
+            X509Chain customChain = new();
+            customChain.ChainPolicy.ExtraStore.Add(trustedRoot);
+            // "tell the X509Chain class that I do trust this root certs and 
it should check just the certs in the chain and nothing else"
+            customChain.ChainPolicy.VerificationFlags = 
X509VerificationFlags.AllowUnknownCertificateAuthority;
+            customChain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
+
+            bool chainValid = customChain.Build(cert2);
+            return chainValid || (tlsProperties.AllowSelfSigned && 
IsSelfSigned(cert2));
         }
     }
 }
diff --git a/csharp/src/Drivers/Apache/Impala/ImpalaStandardConnection.cs 
b/csharp/src/Drivers/Apache/Impala/ImpalaStandardConnection.cs
index 6cbef924a..a87a7973b 100644
--- a/csharp/src/Drivers/Apache/Impala/ImpalaStandardConnection.cs
+++ b/csharp/src/Drivers/Apache/Impala/ImpalaStandardConnection.cs
@@ -18,6 +18,7 @@
 using System;
 using System.Collections.Generic;
 using System.Net;
+using System.Net.Security;
 using System.Security.Cryptography.X509Certificates;
 using System.Threading;
 using System.Threading.Tasks;
@@ -115,13 +116,14 @@ namespace Apache.Arrow.Adbc.Drivers.Apache.Impala
             TTransport transport;
             if (TlsOptions.IsTlsEnabled)
             {
+                RemoteCertificateValidationCallback certValidator = (sender, 
cert, chain, errors) => HiveServer2TlsImpl.ValidateCertificate(cert, errors, 
TlsOptions);
                 if (IPAddress.TryParse(hostName!, out var address))
                 {
-                    transport = new TTlsSocketTransport(address!, 
int.Parse(port!), config: new(), 0, 
!string.IsNullOrEmpty(TlsOptions.TrustedCertificatePath) ? new 
X509Certificate2(TlsOptions.TrustedCertificatePath!) : null, certValidator: 
HiveServer2TlsImpl.GetCertificateValidator(TlsOptions));
+                    transport = new TTlsSocketTransport(address!, 
int.Parse(port!), config: new(), 0, null, certValidator: certValidator);
                 }
                 else
                 {
-                    transport = new TTlsSocketTransport(hostName!, 
int.Parse(port!), config: new(), 0, 
!string.IsNullOrEmpty(TlsOptions.TrustedCertificatePath) ? new 
X509Certificate2(TlsOptions.TrustedCertificatePath!) : null, certValidator: 
HiveServer2TlsImpl.GetCertificateValidator(TlsOptions));
+                    transport = new TTlsSocketTransport(hostName!, 
int.Parse(port!), config: new(), 0, null, certValidator: certValidator);
                 }
             }
             else
diff --git a/csharp/test/Drivers/Apache/Hive2/HiveServer2TlsImplTest.cs 
b/csharp/test/Drivers/Apache/Hive2/HiveServer2TlsImplTest.cs
index f707b992f..7ae6a207f 100644
--- a/csharp/test/Drivers/Apache/Hive2/HiveServer2TlsImplTest.cs
+++ b/csharp/test/Drivers/Apache/Hive2/HiveServer2TlsImplTest.cs
@@ -64,8 +64,6 @@ namespace Apache.Arrow.Adbc.Tests.Drivers.Apache.Hive2
             yield return new object?[] { new Dictionary<string, string> { { 
HttpTlsOptions.IsTlsEnabled, "True" } }, new TlsProperties { IsTlsEnabled = 
true, DisableServerCertificateValidation = false, AllowSelfSigned = false, 
AllowHostnameMismatch = false } };
             yield return new object?[] { new Dictionary<string, string> { { 
HttpTlsOptions.IsTlsEnabled, "tRUe" }, { HttpTlsOptions.AllowSelfSigned, "true" 
} }, new TlsProperties { IsTlsEnabled = true, 
DisableServerCertificateValidation = false, AllowSelfSigned = true, 
AllowHostnameMismatch = false } };
             yield return new object?[] { new Dictionary<string, string> { { 
HttpTlsOptions.IsTlsEnabled, "TruE" }, { HttpTlsOptions.AllowSelfSigned, "True" 
}, { HttpTlsOptions.AllowHostnameMismatch, "True" } }, new TlsProperties { 
IsTlsEnabled = true, DisableServerCertificateValidation = false, 
AllowSelfSigned = true, AllowHostnameMismatch = true } };
-            // certificate path is ignored if self signed is not allowed
-            yield return new object?[] { new Dictionary<string, string> { { 
HttpTlsOptions.IsTlsEnabled, "True" }, { HttpTlsOptions.AllowSelfSigned, 
"False" }, { HttpTlsOptions.AllowHostnameMismatch, "True" }, { 
HttpTlsOptions.TrustedCertificatePath, "" } }, new TlsProperties { IsTlsEnabled 
= true, DisableServerCertificateValidation = false, AllowSelfSigned = false, 
AllowHostnameMismatch = true } };
             // invalid certificate path
             yield return new object?[] { new Dictionary<string, string> { { 
HttpTlsOptions.IsTlsEnabled, "True" }, { HttpTlsOptions.AllowSelfSigned, "True" 
}, { HttpTlsOptions.AllowHostnameMismatch, "True" }, { 
HttpTlsOptions.TrustedCertificatePath, "" } }, null, 
typeof(FileNotFoundException) };
         }
@@ -86,8 +84,6 @@ namespace Apache.Arrow.Adbc.Tests.Drivers.Apache.Hive2
             yield return new object?[] { new Dictionary<string, string> { { 
StandardTlsOptions.IsTlsEnabled, "True" } }, new TlsProperties { IsTlsEnabled = 
true, DisableServerCertificateValidation = false, AllowSelfSigned = false, 
AllowHostnameMismatch = false } };
             yield return new object?[] { new Dictionary<string, string> { { 
StandardTlsOptions.IsTlsEnabled, "tRUe" }, { 
StandardTlsOptions.AllowSelfSigned, "true" } }, new TlsProperties { 
IsTlsEnabled = true, DisableServerCertificateValidation = false, 
AllowSelfSigned = true, AllowHostnameMismatch = false } };
             yield return new object?[] { new Dictionary<string, string> { { 
StandardTlsOptions.IsTlsEnabled, "TruE" }, { 
StandardTlsOptions.AllowSelfSigned, "True" }, { 
StandardTlsOptions.AllowHostnameMismatch, "True" } }, new TlsProperties { 
IsTlsEnabled = true, DisableServerCertificateValidation = false, 
AllowSelfSigned = true, AllowHostnameMismatch = true } };
-            // certificate path is ignored if self signed is not allowed
-            yield return new object?[] { new Dictionary<string, string> { { 
StandardTlsOptions.IsTlsEnabled, "True" }, { 
StandardTlsOptions.AllowSelfSigned, "False" }, { 
StandardTlsOptions.AllowHostnameMismatch, "True" }, { 
StandardTlsOptions.TrustedCertificatePath, "" } }, new TlsProperties { 
IsTlsEnabled = true, DisableServerCertificateValidation = false, 
AllowSelfSigned = false, AllowHostnameMismatch = true } };
             // invalid certificate path
             yield return new object?[] { new Dictionary<string, string> { { 
StandardTlsOptions.IsTlsEnabled, "True" }, { 
StandardTlsOptions.AllowSelfSigned, "True" }, { 
StandardTlsOptions.AllowHostnameMismatch, "True" }, { 
StandardTlsOptions.TrustedCertificatePath, "" } }, null, 
typeof(FileNotFoundException) };
         }

Reply via email to