Add HistorianClientOptions.AllowUntrustedServerCertificate

When true, the SDK's WCF channel factories accept the server's X.509
certificate without chain validation. Intended for connecting to
development / on-prem Historians whose /HistCert endpoint presents an
installer-generated self-signed cert that isn't in the local trust
store. Particularly relevant on Linux: .NET WCF on Linux does its own
X509Chain validation that doesn't honor the system CA bundle, so even
after `update-ca-certificates` succeeds the cert binding still rejects
the server. With this option set, custom certificate validator accepts
any cert and revocation checking is disabled.

Default false. Centralized in HistorianWcfClientCredentialsHelper.Configure
and applied at every ChannelFactory<T> instantiation in the WCF layer
(no-op when the option is false). 171/171 Windows tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-04 23:05:32 -04:00
parent 92d4110142
commit d3e5bf09b6
5 changed files with 53 additions and 0 deletions
@@ -27,4 +27,15 @@ public sealed class HistorianClientOptions
public HistorianTransport Transport { get; init; } = HistorianTransport.LocalPipe;
public string TargetSpn { get; init; } = @"NT SERVICE\aahClientAccessPoint";
/// <summary>
/// When true, the WCF channel factories used by the SDK accept the server's
/// X.509 certificate without chain validation. Useful when connecting to a
/// development / on-prem Historian whose <c>/HistCert</c> endpoint presents an
/// installer-generated self-signed cert that isn't in the local trust store
/// (notably .NET WCF on Linux ignores the system CA bundle for its own
/// X509Chain checks). Default false; do not enable in production where the
/// server's identity matters.
/// </summary>
public bool AllowUntrustedServerCertificate { get; init; }
}
@@ -45,6 +45,7 @@ internal static class HistorianWcfAuthChainHelper
Action<IHistoryServiceContract2, OpenConnectionContext>? additionalSetup = null)
{
ChannelFactory<IHistoryServiceContract2> historyFactory = new(historyBinding, historyEndpoint);
HistorianWcfClientCredentialsHelper.Configure(historyFactory, options);
historyFactory.Endpoint.EndpointBehaviors.Add(new HistorianWcfHistAddressingBehavior());
if (HistorianWcfMessageCaptureBehavior.IsEnabled)
{
@@ -0,0 +1,38 @@
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Security;
namespace AVEVA.Historian.Client.Wcf;
/// <remarks>
/// Centralizes per-channel-factory credentials configuration that's not bound to a
/// single binding type. Today this covers <c>ServerCertificateValidation</c> for the
/// cert-transport binding when callers opt into <see cref="HistorianClientOptions.AllowUntrustedServerCertificate"/>.
/// Apply at every ChannelFactory&lt;T&gt; instantiation point in the WCF layer.
/// </remarks>
internal static class HistorianWcfClientCredentialsHelper
{
public static void Configure<TChannel>(ChannelFactory<TChannel> factory, HistorianClientOptions options)
{
ArgumentNullException.ThrowIfNull(factory);
ArgumentNullException.ThrowIfNull(options);
if (options.AllowUntrustedServerCertificate)
{
factory.Credentials.ServiceCertificate.SslCertificateAuthentication = new X509ServiceCertificateAuthentication
{
CertificateValidationMode = X509CertificateValidationMode.Custom,
CustomCertificateValidator = AcceptAnyCertificateValidator.Instance,
RevocationMode = X509RevocationMode.NoCheck,
};
}
}
private sealed class AcceptAnyCertificateValidator : X509CertificateValidator
{
public static readonly AcceptAnyCertificateValidator Instance = new();
public override void Validate(X509Certificate2 certificate) { }
}
}
@@ -111,6 +111,7 @@ internal sealed class HistorianWcfEventOrchestrator
CancellationToken cancellationToken)
{
ChannelFactory<IRetrievalServiceContract4> factory = new(binding, retrievalEndpoint);
HistorianWcfClientCredentialsHelper.Configure(factory, _options);
try
{
@@ -179,6 +179,7 @@ internal sealed class HistorianWcfReadOrchestrator
CancellationToken cancellationToken)
{
ChannelFactory<IRetrievalServiceContract2> retrievalFactory = new(binding, retrievalEndpoint);
HistorianWcfClientCredentialsHelper.Configure(retrievalFactory, _options);
try
{
@@ -280,6 +281,7 @@ internal sealed class HistorianWcfReadOrchestrator
CancellationToken cancellationToken)
{
ChannelFactory<IRetrievalServiceContract2> retrievalFactory = new(binding, retrievalEndpoint);
HistorianWcfClientCredentialsHelper.Configure(retrievalFactory, _options);
try
{