fix(opcuaclient): never pass checkDomain for a None endpoint; document both SDK check sites (#816 review)
SDK 2.0.1 checks the server host name in TWO places: Session.OpenAsync (after the chain is trusted) and DefaultSessionFactory.CreateChannelAsync, against the certificate in the GetEndpoints reply and before any security-mode test. opc-plc sends a certificate on its None endpoint, so `connect -u opc.tcp://10.100.0.35:50000 --validate-host-name` was refused on an unauthenticated channel, where a host check proves nothing. All three call sites now pass checkDomain only when the selected endpoint's SecurityMode != None: OpcUaEndpointSelection.CheckDomainFor for the driver (OpenSessionOnEndpointAsync, so the failover sweep too) and the AdminUI browser; the same expression in Client.Shared ConnectToEndpointAsync (shared by failover). New live tests: driver None + flag on vs :50000 reads; browser None + flag on vs the NAT fixture browses; the secure + flag refusals still hold. Docs: name both check sites; a zone-qualified IPv6 host never matches (FindDomain compares the zone too), so link-local needs validateServerHostName=false; the match is loose (first label alone, subject DC= parts count, no wildcards), which caveats the CA recommendation.
This commit is contained in:
+4
-2
@@ -115,8 +115,10 @@ public sealed class OpcUaClientDriverBrowser : IDriverBrowser
|
||||
configuredEndpoint,
|
||||
updateBeforeConnect: false,
|
||||
// Gitea #816: the same host-name policy as the deployed driver (the SDK's overload without this
|
||||
// argument hard-codes false), checked against the configured host the endpoint was rewritten to.
|
||||
checkDomain: opts.CertificateValidation.EffectiveValidateServerHostName,
|
||||
// argument hard-codes false), checked against the configured host the endpoint was rewritten to —
|
||||
// and, like the driver, never on a None endpoint.
|
||||
checkDomain: OpcUaEndpointSelection.CheckDomainFor(
|
||||
endpointDesc, opts.CertificateValidation.EffectiveValidateServerHostName),
|
||||
sessionName: "OtOpcUa AdminUI Browse",
|
||||
(uint)opts.SessionTimeout.TotalMilliseconds,
|
||||
identity,
|
||||
|
||||
+6
-3
@@ -251,9 +251,12 @@ public sealed record OpcUaCertificateValidationOptions
|
||||
/// <item><c>false</c> = explicit opt-out — the NAT / port-publish / IP-addressed case, where the server's
|
||||
/// certificate names its private host and the reachable address is something else.</item>
|
||||
/// </list>
|
||||
/// Only applies to secured sessions (a <c>None</c> channel validates no server certificate at all), and
|
||||
/// <see cref="OpcUaClientDriverOptions.AutoAcceptCertificates"/> accepts a host-name mismatch like any other
|
||||
/// certificate error.
|
||||
/// Only applies to Sign / SignAndEncrypt endpoints: the driver and browser never pass the SDK's
|
||||
/// <c>checkDomain</c> for a <c>None</c> endpoint, because SDK 2.0.1 would otherwise check at channel creation
|
||||
/// against the GetEndpoints certificate even there. <see cref="OpcUaClientDriverOptions.AutoAcceptCertificates"/>
|
||||
/// accepts a host-name mismatch like any other certificate error. A zone-qualified IPv6 host
|
||||
/// (<c>[fe80::1%eth0]</c>) never matches (the SDK compares the host WITH its zone against the certificate's
|
||||
/// zone-less IP SANs), so a link-local endpoint needs <c>false</c>.
|
||||
/// </summary>
|
||||
public bool? ValidateServerHostName { get; init; }
|
||||
|
||||
|
||||
@@ -46,6 +46,23 @@ public static class OpcUaEndpointSelection
|
||||
return match;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #816 — the <c>checkDomain</c> to hand the SDK for <paramref name="selected"/>: the operator's choice, but
|
||||
/// never on a <see cref="MessageSecurityMode.None"/> endpoint. SDK 2.0.1 checks the host in TWO places — in
|
||||
/// <c>DefaultSessionFactory.CreateChannelAsync</c>, against the certificate in the GetEndpoints reply, BEFORE any
|
||||
/// security-mode test (opc-plc sends one even on its None endpoint), and in <c>Session.OpenAsync</c> once the
|
||||
/// chain is trusted — so passing <c>true</c> on a None session refused it on a certificate the channel never
|
||||
/// uses. A host-name check on an unauthenticated channel proves nothing.
|
||||
/// </summary>
|
||||
/// <param name="selected">The endpoint the session will open on.</param>
|
||||
/// <param name="validateServerHostName">The configured (effective) host-name policy.</param>
|
||||
/// <returns>The <c>checkDomain</c> argument.</returns>
|
||||
public static bool CheckDomainFor(EndpointDescription selected, bool validateServerHostName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(selected);
|
||||
return validateServerHostName && selected.SecurityMode != MessageSecurityMode.None;
|
||||
}
|
||||
|
||||
/// <summary>Convert a driver <see cref="OpcUaSecurityPolicy"/> to the OPC UA policy URI.</summary>
|
||||
/// <param name="policy">The driver security policy to map.</param>
|
||||
/// <returns>The corresponding OPC UA security policy URI.</returns>
|
||||
|
||||
@@ -1061,12 +1061,14 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
|
||||
// Gitea #816: the SDK's 8-argument CreateAsync hard-codes checkDomain:false, so before #816 the server
|
||||
// certificate's host name was never checked. The check (when enabled) runs against endpoint.EndpointUrl —
|
||||
// the CONFIGURED host, because SelectMatching rewrote the advertised one.
|
||||
// the CONFIGURED host, because SelectMatching rewrote the advertised one — and never on a None endpoint
|
||||
// (CheckDomainFor; the SDK would otherwise check at channel creation even there).
|
||||
var session = await new DefaultSessionFactory(_sdkTelemetry).CreateAsync(
|
||||
appConfig,
|
||||
endpoint,
|
||||
false, // updateBeforeConnect
|
||||
_options.CertificateValidation.EffectiveValidateServerHostName, // checkDomain (#816)
|
||||
OpcUaEndpointSelection.CheckDomainFor( // checkDomain (#816)
|
||||
selected, _options.CertificateValidation.EffectiveValidateServerHostName),
|
||||
_options.SessionName,
|
||||
(uint)_options.SessionTimeout.TotalMilliseconds,
|
||||
identity,
|
||||
|
||||
Reference in New Issue
Block a user