diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Ipc/TcpFrameServer.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Ipc/TcpFrameServer.cs index 313f44c7..cdaa4919 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Ipc/TcpFrameServer.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Ipc/TcpFrameServer.cs @@ -69,6 +69,7 @@ public sealed class TcpFrameServer : IDisposable try { client = await _listener!.AcceptTcpClientAsync().ConfigureAwait(false); } catch (ObjectDisposedException) when (linked.Token.IsCancellationRequested) { throw new OperationCanceledException(linked.Token); } catch (InvalidOperationException) when (linked.Token.IsCancellationRequested) { throw new OperationCanceledException(linked.Token); } + catch (SocketException) when (linked.Token.IsCancellationRequested) { throw new OperationCanceledException(linked.Token); } using (client) { @@ -107,6 +108,7 @@ public sealed class TcpFrameServer : IDisposable await writer.WriteAsync(MessageKind.HelloAck, new HelloAck { Accepted = false, RejectReason = $"major-version-mismatch-peer={hello.ProtocolMajor}-server={Hello.CurrentMajor}" }, linked.Token).ConfigureAwait(false); + _logger.Warning("Sidecar TCP Hello rejected: major mismatch peer={Peer} server={Server}", hello.ProtocolMajor, Hello.CurrentMajor); return; } await writer.WriteAsync(MessageKind.HelloAck, diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/Ipc/TcpRoundTripTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/Ipc/TcpRoundTripTests.cs index 1b95aa41..c405c01b 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/Ipc/TcpRoundTripTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/Ipc/TcpRoundTripTests.cs @@ -97,7 +97,7 @@ public sealed class TcpRoundTripTests userCertificateValidationCallback: (_, cert, _, _) => cert is not null && string.Equals( - new X509Certificate2(cert).Thumbprint, + cert.GetCertHashString(), expectedThumbprint, StringComparison.OrdinalIgnoreCase)); await ssl.AuthenticateAsClientAsync("otopcua-historian-sidecar-test", clientCertificates: null, @@ -172,6 +172,38 @@ public sealed class TcpRoundTripTests await serverTask; } + /// + /// TLS: when the client pins a wrong thumbprint the validation callback returns false, + /// causing to throw + /// before any Hello is exchanged. + /// + [Fact] + public async Task Tls_BadThumbprint_AuthenticationFails() + { + using var cts = new CancellationTokenSource(Timeout); + using var cert = MakeSelfSignedCert(); + using var server = new TcpFrameServer(IPAddress.Loopback, 0, "shh", tlsCert: cert, Quiet); + var serverTask = server.RunOneConnectionAsync(new EchoHandler(), cts.Token); + + using var client = new TcpClient(); + await client.ConnectAsync(IPAddress.Loopback, server.BoundPort); + + // Deliberately pin the wrong thumbprint — all zeros. + const string wrongThumbprint = "0000000000000000000000000000000000000000"; + var ssl = new SslStream(client.GetStream(), leaveInnerStreamOpen: false, + userCertificateValidationCallback: (_, serverCert, _, _) => + serverCert is not null && + string.Equals(serverCert.GetCertHashString(), wrongThumbprint, StringComparison.OrdinalIgnoreCase)); + + await Should.ThrowAsync(async () => + await ssl.AuthenticateAsClientAsync("otopcua-historian-sidecar-test", clientCertificates: null, + enabledSslProtocols: SslProtocols.Tls12, checkCertificateRevocation: false)); + + ssl.Dispose(); + // Server will see the broken TLS handshake and end the connection; let it finish. + try { await serverTask; } catch { /* server may throw on the aborted TLS */ } + } + /// Bad secret: Hello is rejected with Accepted=false and the shared-secret-mismatch reason. [Fact] public async Task BadSecret_HelloRejected()