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

blankensteiner pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar-dotpulsar.git


The following commit(s) were added to refs/heads/master by this push:
     new 2a17817  Added support for custom remote certificate validation
2a17817 is described below

commit 2a17817abb6504065e7957891223446d00185dbf
Author: Daniel Blankensteiner <[email protected]>
AuthorDate: Mon Nov 17 13:39:35 2025 +0100

    Added support for custom remote certificate validation
---
 CHANGELOG.md                                       |  1 +
 src/DotPulsar/Abstractions/IPulsarClientBuilder.cs |  5 ++
 .../Abstractions/IValidateRemoteCertificate.cs     | 29 ++++++++++
 .../Extensions/PulsarClientBuilderExtensions.cs    | 18 +++---
 src/DotPulsar/Internal/Connector.cs                | 49 ++---------------
 .../Internal/DefaultRemoteCertificateValidator.cs  | 64 ++++++++++++++++++++++
 .../Internal/FuncRemoteCertificateValidator.cs     | 28 ++++++++++
 src/DotPulsar/Internal/PulsarClientBuilder.cs      | 18 ++++--
 8 files changed, 156 insertions(+), 56 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d42ae69..869e534 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ The format is based on [Keep a 
Changelog](https://keepachangelog.com/en/1.1.0/)
 ### Added
 
 - .NET 10 added as a target framework
+- Support for custom remote certificate validation
 
 ### Changed
 
diff --git a/src/DotPulsar/Abstractions/IPulsarClientBuilder.cs 
b/src/DotPulsar/Abstractions/IPulsarClientBuilder.cs
index b122bd2..10735fa 100644
--- a/src/DotPulsar/Abstractions/IPulsarClientBuilder.cs
+++ b/src/DotPulsar/Abstractions/IPulsarClientBuilder.cs
@@ -56,6 +56,11 @@ public interface IPulsarClientBuilder
     /// </summary>
     IPulsarClientBuilder ListenerName(string listenerName);
 
+    /// <summary>
+    /// Register a custom remote certificate validator. This is optional.
+    /// </summary>
+    IPulsarClientBuilder 
RemoteCertificateValidation(IValidateRemoteCertificate 
remoteCertificateValidator);
+
     /// <summary>
     /// The time to wait before retrying an operation or a reconnect. The 
default is 3 seconds.
     /// </summary>
diff --git a/src/DotPulsar/Abstractions/IValidateRemoteCertificate.cs 
b/src/DotPulsar/Abstractions/IValidateRemoteCertificate.cs
new file mode 100644
index 0000000..e48f1f2
--- /dev/null
+++ b/src/DotPulsar/Abstractions/IValidateRemoteCertificate.cs
@@ -0,0 +1,29 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * 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.
+ */
+
+namespace DotPulsar.Abstractions;
+
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+
+/// <summary>
+/// Abstraction for implementing remote certificate validation
+/// </summary>
+public interface IValidateRemoteCertificate
+{
+    /// <summary>
+    /// Callback triggered when creating an SslStream
+    /// </summary>
+    bool Validate(object sender, X509Certificate? certificate, X509Chain? 
chain, SslPolicyErrors sslPolicyErrors);
+}
diff --git a/src/DotPulsar/Extensions/PulsarClientBuilderExtensions.cs 
b/src/DotPulsar/Extensions/PulsarClientBuilderExtensions.cs
index f05bcc0..4d3a0ce 100644
--- a/src/DotPulsar/Extensions/PulsarClientBuilderExtensions.cs
+++ b/src/DotPulsar/Extensions/PulsarClientBuilderExtensions.cs
@@ -16,6 +16,8 @@ namespace DotPulsar.Extensions;
 
 using DotPulsar.Abstractions;
 using DotPulsar.Internal;
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
 
 /// <summary>
 /// Extensions for IPulsarClientBuilder.
@@ -26,17 +28,17 @@ public static class PulsarClientBuilderExtensions
     /// Register a custom exception handler that will be invoked before the 
default exception handler.
     /// </summary>
     public static IPulsarClientBuilder ExceptionHandler(this 
IPulsarClientBuilder builder, Action<ExceptionContext> exceptionHandler)
-    {
-        builder.ExceptionHandler(new ActionExceptionHandler(exceptionHandler));
-        return builder;
-    }
+        => builder.ExceptionHandler(new 
ActionExceptionHandler(exceptionHandler));
 
     /// <summary>
     /// Register a custom exception handler that will be invoked before the 
default exception handler.
     /// </summary>
     public static IPulsarClientBuilder ExceptionHandler(this 
IPulsarClientBuilder builder, Func<ExceptionContext, ValueTask> 
exceptionHandler)
-    {
-        builder.ExceptionHandler(new FuncExceptionHandler(exceptionHandler));
-        return builder;
-    }
+        => builder.ExceptionHandler(new 
FuncExceptionHandler(exceptionHandler));
+
+    /// <summary>
+    /// Register a custom remote certificate validator.
+    /// </summary>
+    public static IPulsarClientBuilder RemoteCertificateValidation(this 
IPulsarClientBuilder builder, Func<object, X509Certificate?, X509Chain?, 
SslPolicyErrors, bool> validator)
+        => builder.RemoteCertificateValidation(new 
FuncRemoteCertificateValidator(validator));
 }
diff --git a/src/DotPulsar/Internal/Connector.cs 
b/src/DotPulsar/Internal/Connector.cs
index d0d5f11..3d5adf7 100644
--- a/src/DotPulsar/Internal/Connector.cs
+++ b/src/DotPulsar/Internal/Connector.cs
@@ -14,7 +14,7 @@
 
 namespace DotPulsar.Internal;
 
-using System;
+using DotPulsar.Abstractions;
 using System.Net;
 using System.Net.Security;
 using System.Net.Sockets;
@@ -23,23 +23,17 @@ using System.Security.Cryptography.X509Certificates;
 
 public sealed class Connector
 {
+    private readonly IValidateRemoteCertificate _remoteCertificateValidator;
     private readonly X509Certificate2Collection _clientCertificates;
-    private readonly X509Certificate2? _trustedCertificateAuthority;
-    private readonly bool _verifyCertificateAuthority;
-    private readonly bool _verifyCertificateName;
     private readonly bool _checkCertificateRevocation;
 
     public Connector(
+        IValidateRemoteCertificate remoteCertificateValidator,
         X509Certificate2Collection clientCertificates,
-        X509Certificate2? trustedCertificateAuthority,
-        bool verifyCertificateAuthority,
-        bool verifyCertificateName,
         bool checkCertificateRevocation)
     {
+        _remoteCertificateValidator = remoteCertificateValidator;
         _clientCertificates = clientCertificates;
-        _trustedCertificateAuthority = trustedCertificateAuthority;
-        _verifyCertificateAuthority = verifyCertificateAuthority;
-        _verifyCertificateName = verifyCertificateName;
         _checkCertificateRevocation = checkCertificateRevocation;
     }
 
@@ -103,7 +97,7 @@ public sealed class Connector
         bool Validate(object sender, X509Certificate? certificate, X509Chain? 
chain, SslPolicyErrors sslPolicyErrors)
         {
             policyErrors = sslPolicyErrors;
-            return ValidateServerCertificate(certificate, chain, 
sslPolicyErrors);
+            return _remoteCertificateValidator.Validate(sender, certificate, 
chain, sslPolicyErrors);
         }
 
         try
@@ -134,7 +128,7 @@ public sealed class Connector
         bool Validate(object sender, X509Certificate? certificate, X509Chain? 
chain, SslPolicyErrors sslPolicyErrors)
         {
             policyErrors = sslPolicyErrors;
-            return ValidateServerCertificate(certificate, chain, 
sslPolicyErrors);
+            return _remoteCertificateValidator.Validate(sender, certificate, 
chain, sslPolicyErrors);
         }
 
         try
@@ -164,35 +158,4 @@ public sealed class Connector
         }
     }
 #endif
-
-    private bool ValidateServerCertificate(X509Certificate? certificate, 
X509Chain? chain, SslPolicyErrors sslPolicyErrors)
-    {
-        if (sslPolicyErrors == SslPolicyErrors.None)
-            return true;
-
-        if 
(sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable))
-            return false;
-
-        if 
(sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch) && 
_verifyCertificateName)
-            return false;
-
-        if 
(sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors) && 
_verifyCertificateAuthority)
-        {
-            if (_trustedCertificateAuthority is null || chain is null || 
certificate is null)
-                return false;
-
-            chain.ChainPolicy.ExtraStore.Add(_trustedCertificateAuthority);
-            _ = chain.Build((X509Certificate2) certificate);
-
-            for (var i = 0; i < chain.ChainElements.Count; i++)
-            {
-                if (chain.ChainElements[i].Certificate.Thumbprint == 
_trustedCertificateAuthority.Thumbprint)
-                    return true;
-            }
-
-            return false;
-        }
-
-        return true;
-    }
 }
diff --git a/src/DotPulsar/Internal/DefaultRemoteCertificateValidator.cs 
b/src/DotPulsar/Internal/DefaultRemoteCertificateValidator.cs
new file mode 100644
index 0000000..0aafc0d
--- /dev/null
+++ b/src/DotPulsar/Internal/DefaultRemoteCertificateValidator.cs
@@ -0,0 +1,64 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * 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.
+ */
+
+namespace DotPulsar.Internal;
+
+using DotPulsar.Abstractions;
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+
+public sealed class DefaultRemoteCertificateValidator : 
IValidateRemoteCertificate
+{
+    private readonly X509Certificate2? _trustedCertificateAuthority;
+    private readonly bool _verifyCertificateAuthority;
+    private readonly bool _verifyCertificateName;
+
+    public DefaultRemoteCertificateValidator(X509Certificate2? 
trustedCertificateAuthority, bool verifyCertificateAuthority, bool 
verifyCertificateName)
+    {
+        _trustedCertificateAuthority = trustedCertificateAuthority;
+        _verifyCertificateAuthority = verifyCertificateAuthority;
+        _verifyCertificateName = verifyCertificateName;
+    }
+
+    public bool Validate(object sender, X509Certificate? certificate, 
X509Chain? chain, SslPolicyErrors sslPolicyErrors)
+    {
+        if (sslPolicyErrors == SslPolicyErrors.None)
+            return true;
+
+        if 
(sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable))
+            return false;
+
+        if 
(sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch) && 
_verifyCertificateName)
+            return false;
+
+        if 
(sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors) && 
_verifyCertificateAuthority)
+        {
+            if (_trustedCertificateAuthority is null || chain is null || 
certificate is null)
+                return false;
+
+            chain.ChainPolicy.ExtraStore.Add(_trustedCertificateAuthority);
+            _ = chain.Build((X509Certificate2) certificate);
+
+            for (var i = 0; i < chain.ChainElements.Count; i++)
+            {
+                if (chain.ChainElements[i].Certificate.Thumbprint == 
_trustedCertificateAuthority.Thumbprint)
+                    return true;
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/src/DotPulsar/Internal/FuncRemoteCertificateValidator.cs 
b/src/DotPulsar/Internal/FuncRemoteCertificateValidator.cs
new file mode 100644
index 0000000..eb51d25
--- /dev/null
+++ b/src/DotPulsar/Internal/FuncRemoteCertificateValidator.cs
@@ -0,0 +1,28 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * 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.
+ */
+
+namespace DotPulsar.Internal;
+
+using DotPulsar.Abstractions;
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+
+public sealed class FuncRemoteCertificateValidator : IValidateRemoteCertificate
+{
+    private readonly Func<object, X509Certificate?, X509Chain?, 
SslPolicyErrors, bool> _validator;
+
+    public FuncRemoteCertificateValidator(Func<object, X509Certificate?, 
X509Chain?, SslPolicyErrors, bool> validator) => _validator = validator;        
+
+    public bool Validate(object sender, X509Certificate? certificate, 
X509Chain? chain, SslPolicyErrors sslPolicyErrors) => _validator(sender, 
certificate, chain, sslPolicyErrors);
+}
diff --git a/src/DotPulsar/Internal/PulsarClientBuilder.cs 
b/src/DotPulsar/Internal/PulsarClientBuilder.cs
index c8113eb..002ea58 100644
--- a/src/DotPulsar/Internal/PulsarClientBuilder.cs
+++ b/src/DotPulsar/Internal/PulsarClientBuilder.cs
@@ -35,6 +35,7 @@ public sealed class PulsarClientBuilder : IPulsarClientBuilder
     private bool _verifyCertificateName;
     private TimeSpan _closeInactiveConnectionsInterval;
     private IAuthentication? _authentication;
+    private IValidateRemoteCertificate? _remoteCertificateValidator;
 
     public PulsarClientBuilder()
     {
@@ -102,6 +103,12 @@ public sealed class PulsarClientBuilder : 
IPulsarClientBuilder
         return this;
     }
 
+    public IPulsarClientBuilder 
RemoteCertificateValidation(IValidateRemoteCertificate 
remoteCertificateValidator)
+    {
+        _remoteCertificateValidator = remoteCertificateValidator;
+        return this;
+    }
+
     public IPulsarClientBuilder RetryInterval(TimeSpan interval)
     {
         _retryInterval = interval;
@@ -147,21 +154,22 @@ public sealed class PulsarClientBuilder : 
IPulsarClientBuilder
             _encryptionPolicy ??= EncryptionPolicy.EnforceUnencrypted;
 
             if (_encryptionPolicy.Value == EncryptionPolicy.EnforceEncrypted)
-                throw new ConnectionSecurityException(
-                    $"The scheme of the ServiceUrl ({_serviceUrl}) is 
'{Constants.PulsarScheme}' and cannot be used with an encryption policy of 
'EnforceEncrypted'");
+                throw new ConnectionSecurityException($"The scheme of the 
ServiceUrl ({_serviceUrl}) is '{Constants.PulsarScheme}' and cannot be used 
with an encryption policy of 'EnforceEncrypted'");
         }
         else if (scheme == Constants.PulsarSslScheme)
         {
             _encryptionPolicy ??= EncryptionPolicy.EnforceEncrypted;
 
             if (_encryptionPolicy.Value == EncryptionPolicy.EnforceUnencrypted)
-                throw new ConnectionSecurityException(
-                    $"The scheme of the ServiceUrl ({_serviceUrl}) is 
'{Constants.PulsarSslScheme}' and cannot be used with an encryption policy of 
'EnforceUnencrypted'");
+                throw new ConnectionSecurityException($"The scheme of the 
ServiceUrl ({_serviceUrl}) is '{Constants.PulsarSslScheme}' and cannot be used 
with an encryption policy of 'EnforceUnencrypted'");
         }
         else
             throw new InvalidSchemeException($"Invalid scheme '{scheme}'. 
Expected '{Constants.PulsarScheme}' or '{Constants.PulsarSslScheme}'");
 
-        var connector = new Connector(_clientCertificates, 
_trustedCertificateAuthority, _verifyCertificateAuthority, 
_verifyCertificateName, _checkCertificateRevocation);
+        if (_remoteCertificateValidator is null)
+            _remoteCertificateValidator = new 
DefaultRemoteCertificateValidator(_trustedCertificateAuthority, 
_verifyCertificateAuthority, _verifyCertificateName);
+
+        var connector = new Connector(_remoteCertificateValidator, 
_clientCertificates, _checkCertificateRevocation);
 
         var exceptionHandlers = new List<IHandleException>(_exceptionHandlers) 
{ new DefaultExceptionHandler() };
         var exceptionHandlerPipeline = new 
ExceptionHandlerPipeline(_retryInterval, exceptionHandlers);

Reply via email to