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) }; }