From 75025752045534a10546dd0f5b857ba69e233b6a Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 4 May 2026 23:08:33 -0400 Subject: [PATCH] Add HistorianClientOptions.ServerDnsIdentity for cert-binding overrides When the server cert's CN/SAN doesn't match the URL host (typical for installer-generated AVEVA Historian certs that claim DNS=localhost even when reached over a LAN IP), WCF rejects the channel with "Identity check failed for outgoing message". Set ServerDnsIdentity to whatever the cert claims (often "localhost") to satisfy the check. The endpoint address for the cert binding is constructed with a DnsEndpointIdentity when the option is non-null. Default null. Pairs with AllowUntrustedServerCertificate so a Linux client can talk to a self-signed dev Historian over RemoteTcpCertificate. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AVEVA.Historian.Client/HistorianClientOptions.cs | 11 +++++++++++ .../Wcf/HistorianWcfBindingFactory.cs | 9 ++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/AVEVA.Historian.Client/HistorianClientOptions.cs b/src/AVEVA.Historian.Client/HistorianClientOptions.cs index 21eed2f..60c6d06 100644 --- a/src/AVEVA.Historian.Client/HistorianClientOptions.cs +++ b/src/AVEVA.Historian.Client/HistorianClientOptions.cs @@ -38,4 +38,15 @@ public sealed class HistorianClientOptions /// server's identity matters. /// public bool AllowUntrustedServerCertificate { get; init; } + + /// + /// Overrides the expected DNS identity in the endpoint address — set this to + /// whatever DNS name the server's certificate actually claims (often + /// localhost on installer-generated AVEVA Historian certificates) when + /// connecting via IP address or a hostname that doesn't match the cert SAN/CN. + /// Without this override WCF rejects the channel with + /// "Identity check failed for outgoing message". Has no effect on transports + /// that don't validate a server certificate. + /// + public string? ServerDnsIdentity { get; init; } } diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianWcfBindingFactory.cs b/src/AVEVA.Historian.Client/Wcf/HistorianWcfBindingFactory.cs index 9838178..7381696 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianWcfBindingFactory.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianWcfBindingFactory.cs @@ -144,19 +144,22 @@ internal static class HistorianWcfBindingFactory CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.Retrieval)), HistorianTransport.RemoteTcpCertificate => ( CreateMdasNetTcpCertificateBinding(timeout), - CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.HistoryCertificate), + CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.HistoryCertificate, options.ServerDnsIdentity), CreateMdasNetTcpBinding(timeout), CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.Retrieval)), _ => throw new NotSupportedException($"Transport {options.Transport} is not supported.") }; } - public static EndpointAddress CreateEndpointAddress(string host, int port, string serviceName) + public static EndpointAddress CreateEndpointAddress(string host, int port, string serviceName, string? dnsIdentity = null) { ArgumentException.ThrowIfNullOrWhiteSpace(host); ArgumentException.ThrowIfNullOrWhiteSpace(serviceName); - return new EndpointAddress($"{Scheme}://{host}:{port}/{serviceName}"); + Uri uri = new($"{Scheme}://{host}:{port}/{serviceName}"); + return string.IsNullOrWhiteSpace(dnsIdentity) + ? new EndpointAddress(uri) + : new EndpointAddress(uri, new DnsEndpointIdentity(dnsIdentity)); } public static EndpointAddress CreatePipeEndpointAddress(string host, string serviceName)