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.
This commit is contained in:
Joseph Doherty
2026-06-01 07:24:07 -04:00
parent 77a9108673
commit 5e01ad9c22
3 changed files with 61 additions and 1 deletions
@@ -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
/// <summary>
/// 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.
/// </summary>
[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));
}
/// <summary>
@@ -40,3 +43,43 @@ public sealed class MxGatewayClientTlsHandlerTests
Assert.Null(handler.SslOptions.RemoteCertificateValidationCallback);
}
}
public sealed class GalaxyRepositoryClientTlsHandlerTests
{
/// <summary>
/// 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.
/// </summary>
[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));
}
/// <summary>
/// Verifies that when RequireCertificateValidation is true, the Galaxy client callback is left null
/// so the OS trust store performs validation.
/// </summary>
[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);
}
}
@@ -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;
@@ -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;