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 {