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) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-04 23:08:33 -04:00
parent d3e5bf09b6
commit 7502575204
2 changed files with 17 additions and 3 deletions
@@ -38,4 +38,15 @@ public sealed class HistorianClientOptions
/// server's identity matters.
/// </summary>
public bool AllowUntrustedServerCertificate { get; init; }
/// <summary>
/// Overrides the expected DNS identity in the endpoint address — set this to
/// whatever DNS name the server's certificate actually claims (often
/// <c>localhost</c> 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.
/// </summary>
public string? ServerDnsIdentity { get; init; }
}
@@ -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)