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