From d3e5bf09b6196b2439eb2f170de386af66fca6f6 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 4 May 2026 23:05:32 -0400 Subject: [PATCH] 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 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) --- .../HistorianClientOptions.cs | 11 ++++++ .../Wcf/HistorianWcfAuthChainHelper.cs | 1 + .../HistorianWcfClientCredentialsHelper.cs | 38 +++++++++++++++++++ .../Wcf/HistorianWcfEventOrchestrator.cs | 1 + .../Wcf/HistorianWcfReadOrchestrator.cs | 2 + 5 files changed, 53 insertions(+) create mode 100644 src/AVEVA.Historian.Client/Wcf/HistorianWcfClientCredentialsHelper.cs diff --git a/src/AVEVA.Historian.Client/HistorianClientOptions.cs b/src/AVEVA.Historian.Client/HistorianClientOptions.cs index e147305..21eed2f 100644 --- a/src/AVEVA.Historian.Client/HistorianClientOptions.cs +++ b/src/AVEVA.Historian.Client/HistorianClientOptions.cs @@ -27,4 +27,15 @@ public sealed class HistorianClientOptions public HistorianTransport Transport { get; init; } = HistorianTransport.LocalPipe; public string TargetSpn { get; init; } = @"NT SERVICE\aahClientAccessPoint"; + + /// + /// 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 /HistCert 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. + /// + public bool AllowUntrustedServerCertificate { get; init; } } diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianWcfAuthChainHelper.cs b/src/AVEVA.Historian.Client/Wcf/HistorianWcfAuthChainHelper.cs index 02a0e59..eaa3396 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianWcfAuthChainHelper.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianWcfAuthChainHelper.cs @@ -45,6 +45,7 @@ internal static class HistorianWcfAuthChainHelper Action? additionalSetup = null) { ChannelFactory historyFactory = new(historyBinding, historyEndpoint); + HistorianWcfClientCredentialsHelper.Configure(historyFactory, options); historyFactory.Endpoint.EndpointBehaviors.Add(new HistorianWcfHistAddressingBehavior()); if (HistorianWcfMessageCaptureBehavior.IsEnabled) { diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianWcfClientCredentialsHelper.cs b/src/AVEVA.Historian.Client/Wcf/HistorianWcfClientCredentialsHelper.cs new file mode 100644 index 0000000..8759671 --- /dev/null +++ b/src/AVEVA.Historian.Client/Wcf/HistorianWcfClientCredentialsHelper.cs @@ -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; + +/// +/// Centralizes per-channel-factory credentials configuration that's not bound to a +/// single binding type. Today this covers ServerCertificateValidation for the +/// cert-transport binding when callers opt into . +/// Apply at every ChannelFactory<T> instantiation point in the WCF layer. +/// +internal static class HistorianWcfClientCredentialsHelper +{ + public static void Configure(ChannelFactory 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) { } + } +} diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianWcfEventOrchestrator.cs b/src/AVEVA.Historian.Client/Wcf/HistorianWcfEventOrchestrator.cs index e37d036..3d79a72 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianWcfEventOrchestrator.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianWcfEventOrchestrator.cs @@ -111,6 +111,7 @@ internal sealed class HistorianWcfEventOrchestrator CancellationToken cancellationToken) { ChannelFactory factory = new(binding, retrievalEndpoint); + HistorianWcfClientCredentialsHelper.Configure(factory, _options); try { diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianWcfReadOrchestrator.cs b/src/AVEVA.Historian.Client/Wcf/HistorianWcfReadOrchestrator.cs index aa147ed..d84da8e 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianWcfReadOrchestrator.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianWcfReadOrchestrator.cs @@ -179,6 +179,7 @@ internal sealed class HistorianWcfReadOrchestrator CancellationToken cancellationToken) { ChannelFactory retrievalFactory = new(binding, retrievalEndpoint); + HistorianWcfClientCredentialsHelper.Configure(retrievalFactory, _options); try { @@ -280,6 +281,7 @@ internal sealed class HistorianWcfReadOrchestrator CancellationToken cancellationToken) { ChannelFactory retrievalFactory = new(binding, retrievalEndpoint); + HistorianWcfClientCredentialsHelper.Configure(retrievalFactory, _options); try {