From 5e01ad9c22c941b58e096509cb6b174609d26556 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 1 Jun 2026 07:24:07 -0400 Subject: [PATCH] fix(client-dotnet): apply lenient TLS to GalaxyRepositoryClient and enforce hostname on CA-pin Mirror MxGatewayClient's three-branch handler structure in GalaxyRepositoryClient (CA-pin / lenient accept-all / OS trust) so the Galaxy endpoint works against the gateway's self-signed cert under the default lenient posture. Expose an internal CreateHttpHandlerForTests seam for unit testing. Add RemoteCertificateNameMismatch rejection at the top of both CA-pinned callbacks so a pinned-CA connection truly verifies the host. Strengthen existing lenient test to invoke the callback and assert it returns true; add mirrored Galaxy-client handler tests. --- .../MxGatewayClientTlsHandlerTests.cs | 43 +++++++++++++++++++ .../GalaxyRepositoryClient.cs | 14 +++++- .../MxGatewayClient.cs | 5 +++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests/MxGatewayClientTlsHandlerTests.cs b/clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests/MxGatewayClientTlsHandlerTests.cs index 3ba09f5..ac6fde9 100644 --- a/clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests/MxGatewayClientTlsHandlerTests.cs +++ b/clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests/MxGatewayClientTlsHandlerTests.cs @@ -1,4 +1,5 @@ using System.Net.Http; +using System.Net.Security; using ZB.MOM.WW.MxGateway.Client; namespace ZB.MOM.WW.MxGateway.Client.Tests; @@ -8,6 +9,7 @@ public sealed class MxGatewayClientTlsHandlerTests /// /// Verifies that when TLS is used with no pinned CA and RequireCertificateValidation is false (default), /// the handler installs an accept-all callback so the gateway's self-signed cert is trusted. + /// The callback must return true regardless of chain errors. /// [Fact] public void Handler_SkipsVerification_WhenTlsAndNoCaPinned() @@ -20,6 +22,7 @@ public sealed class MxGatewayClientTlsHandlerTests }; using SocketsHttpHandler handler = MxGatewayClient.CreateHttpHandlerForTests(options); Assert.NotNull(handler.SslOptions.RemoteCertificateValidationCallback); + Assert.True(handler.SslOptions.RemoteCertificateValidationCallback!(null!, null!, null, SslPolicyErrors.RemoteCertificateChainErrors)); } /// @@ -40,3 +43,43 @@ public sealed class MxGatewayClientTlsHandlerTests Assert.Null(handler.SslOptions.RemoteCertificateValidationCallback); } } + +public sealed class GalaxyRepositoryClientTlsHandlerTests +{ + /// + /// Verifies that when TLS is used with no pinned CA and RequireCertificateValidation is false (default), + /// the Galaxy client handler installs an accept-all callback so the gateway's self-signed cert is trusted. + /// The callback must return true regardless of chain errors. + /// + [Fact] + public void Handler_SkipsVerification_WhenTlsAndNoCaPinned() + { + MxGatewayClientOptions options = new() + { + Endpoint = new Uri("https://localhost:5120"), + ApiKey = "k", + UseTls = true, + }; + using SocketsHttpHandler handler = GalaxyRepositoryClient.CreateHttpHandlerForTests(options); + Assert.NotNull(handler.SslOptions.RemoteCertificateValidationCallback); + Assert.True(handler.SslOptions.RemoteCertificateValidationCallback!(null!, null!, null, SslPolicyErrors.RemoteCertificateChainErrors)); + } + + /// + /// Verifies that when RequireCertificateValidation is true, the Galaxy client callback is left null + /// so the OS trust store performs validation. + /// + [Fact] + public void Handler_KeepsDefaultVerification_WhenRequireCertificateValidation() + { + MxGatewayClientOptions options = new() + { + Endpoint = new Uri("https://localhost:5120"), + ApiKey = "k", + UseTls = true, + RequireCertificateValidation = true, + }; + using SocketsHttpHandler handler = GalaxyRepositoryClient.CreateHttpHandlerForTests(options); + Assert.Null(handler.SslOptions.RemoteCertificateValidationCallback); + } +} diff --git a/clients/dotnet/ZB.MOM.WW.MxGateway.Client/GalaxyRepositoryClient.cs b/clients/dotnet/ZB.MOM.WW.MxGateway.Client/GalaxyRepositoryClient.cs index ac5029b..7d05638 100644 --- a/clients/dotnet/ZB.MOM.WW.MxGateway.Client/GalaxyRepositoryClient.cs +++ b/clients/dotnet/ZB.MOM.WW.MxGateway.Client/GalaxyRepositoryClient.cs @@ -490,7 +490,10 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable .ConfigureAwait(false); } - private static HttpMessageHandler CreateHttpHandler(MxGatewayClientOptions options) + private static HttpMessageHandler CreateHttpHandler(MxGatewayClientOptions options) => + CreateHttpHandlerForTests(options); + + internal static SocketsHttpHandler CreateHttpHandlerForTests(MxGatewayClientOptions options) { SocketsHttpHandler handler = new() { @@ -510,6 +513,11 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable X509Certificate2 trustedRoot = X509CertificateLoader.LoadCertificateFromFile(options.CaCertificatePath); handler.SslOptions.RemoteCertificateValidationCallback = (_, certificate, chain, errors) => { + if ((errors & System.Net.Security.SslPolicyErrors.RemoteCertificateNameMismatch) != 0) + { + return false; + } + if (certificate is null) { return false; @@ -525,6 +533,10 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable return customChain.Build(certificateToValidate); }; } + else if (!options.RequireCertificateValidation) + { + handler.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true; + } } return handler; diff --git a/clients/dotnet/ZB.MOM.WW.MxGateway.Client/MxGatewayClient.cs b/clients/dotnet/ZB.MOM.WW.MxGateway.Client/MxGatewayClient.cs index 20c35a4..6d9ab58 100644 --- a/clients/dotnet/ZB.MOM.WW.MxGateway.Client/MxGatewayClient.cs +++ b/clients/dotnet/ZB.MOM.WW.MxGateway.Client/MxGatewayClient.cs @@ -338,6 +338,11 @@ public sealed class MxGatewayClient : IAsyncDisposable X509Certificate2 trustedRoot = X509CertificateLoader.LoadCertificateFromFile(options.CaCertificatePath); handler.SslOptions.RemoteCertificateValidationCallback = (_, certificate, chain, errors) => { + if ((errors & System.Net.Security.SslPolicyErrors.RemoteCertificateNameMismatch) != 0) + { + return false; + } + if (certificate is null) { return false;