LmxProxy is no longer needed. Moved the entire lmxproxy/ workspace, DCL adapter files, and related docs to deprecated/. Removed LmxProxy registration from DataConnectionFactory, project reference from DCL, protocol option from UI, and cleaned up all requirement docs.
185 lines
6.9 KiB
C#
185 lines
6.9 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Security;
|
|
using System.Security.Authentication;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using Grpc.Net.Client;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace ZB.MOM.WW.LmxProxy.Client.Security;
|
|
|
|
internal static class GrpcChannelFactory
|
|
{
|
|
private const string Http2UnencryptedSwitch = "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport";
|
|
|
|
static GrpcChannelFactory()
|
|
{
|
|
AppContext.SetSwitch(Http2UnencryptedSwitch, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a gRPC channel with optional TLS configuration.
|
|
/// </summary>
|
|
/// <param name="address">The server address.</param>
|
|
/// <param name="tlsConfiguration">Optional TLS configuration.</param>
|
|
/// <param name="logger">The logger.</param>
|
|
/// <returns>A configured gRPC channel.</returns>
|
|
public static GrpcChannel CreateChannel(Uri address, ClientTlsConfiguration? tlsConfiguration, ILogger logger)
|
|
{
|
|
var options = new GrpcChannelOptions
|
|
{
|
|
HttpHandler = CreateHttpHandler(tlsConfiguration, logger)
|
|
};
|
|
|
|
return GrpcChannel.ForAddress(address, options);
|
|
}
|
|
|
|
private static HttpMessageHandler CreateHttpHandler(ClientTlsConfiguration? tlsConfiguration, ILogger logger)
|
|
{
|
|
var handler = new SocketsHttpHandler
|
|
{
|
|
AutomaticDecompression = DecompressionMethods.None,
|
|
AllowAutoRedirect = false,
|
|
EnableMultipleHttp2Connections = true
|
|
};
|
|
|
|
if (tlsConfiguration?.UseTls == true)
|
|
{
|
|
ConfigureTls(handler, tlsConfiguration, logger);
|
|
}
|
|
|
|
return handler;
|
|
}
|
|
|
|
private static void ConfigureTls(SocketsHttpHandler handler, ClientTlsConfiguration tlsConfiguration, ILogger logger)
|
|
{
|
|
SslClientAuthenticationOptions sslOptions = handler.SslOptions;
|
|
sslOptions.EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13;
|
|
|
|
if (!string.IsNullOrWhiteSpace(tlsConfiguration.ServerNameOverride))
|
|
{
|
|
sslOptions.TargetHost = tlsConfiguration.ServerNameOverride;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(tlsConfiguration.ClientCertificatePath) &&
|
|
!string.IsNullOrWhiteSpace(tlsConfiguration.ClientKeyPath))
|
|
{
|
|
try
|
|
{
|
|
var clientCertificate = X509Certificate2.CreateFromPemFile(
|
|
tlsConfiguration.ClientCertificatePath,
|
|
tlsConfiguration.ClientKeyPath);
|
|
clientCertificate = new X509Certificate2(clientCertificate.Export(X509ContentType.Pfx));
|
|
|
|
sslOptions.ClientCertificates ??= new X509CertificateCollection();
|
|
sslOptions.ClientCertificates.Add(clientCertificate);
|
|
logger.LogInformation("Configured client certificate for mutual TLS ({CertificatePath})", tlsConfiguration.ClientCertificatePath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Failed to load client certificate from {CertificatePath}", tlsConfiguration.ClientCertificatePath);
|
|
}
|
|
}
|
|
|
|
sslOptions.RemoteCertificateValidationCallback = (_, certificate, chain, sslPolicyErrors) =>
|
|
ValidateServerCertificate(tlsConfiguration, logger, certificate, chain, sslPolicyErrors);
|
|
}
|
|
|
|
private static bool ValidateServerCertificate(
|
|
ClientTlsConfiguration tlsConfiguration,
|
|
ILogger logger,
|
|
X509Certificate? certificate,
|
|
X509Chain? chain,
|
|
SslPolicyErrors sslPolicyErrors)
|
|
{
|
|
if (tlsConfiguration.IgnoreAllCertificateErrors)
|
|
{
|
|
logger.LogWarning("SECURITY WARNING: Ignoring all certificate validation errors for LmxProxy gRPC connection.");
|
|
return true;
|
|
}
|
|
|
|
if (certificate is null)
|
|
{
|
|
logger.LogWarning("Server certificate was null.");
|
|
return false;
|
|
}
|
|
|
|
if (!tlsConfiguration.ValidateServerCertificate)
|
|
{
|
|
logger.LogWarning("SECURITY WARNING: Server certificate validation disabled for LmxProxy gRPC connection.");
|
|
return true;
|
|
}
|
|
|
|
X509Certificate2 certificate2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
|
|
|
|
if (!string.IsNullOrWhiteSpace(tlsConfiguration.ServerNameOverride))
|
|
{
|
|
string dnsName = certificate2.GetNameInfo(X509NameType.DnsName, forIssuer: false);
|
|
if (!string.Equals(dnsName, tlsConfiguration.ServerNameOverride, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
logger.LogWarning("Server certificate subject '{Subject}' does not match expected host '{ExpectedHost}'",
|
|
dnsName, tlsConfiguration.ServerNameOverride);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
using X509Chain validationChain = chain ?? new X509Chain();
|
|
validationChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
|
|
validationChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
|
|
|
|
if (!string.IsNullOrWhiteSpace(tlsConfiguration.ServerCaCertificatePath) &&
|
|
File.Exists(tlsConfiguration.ServerCaCertificatePath))
|
|
{
|
|
try
|
|
{
|
|
X509Certificate2 ca = LoadCertificate(tlsConfiguration.ServerCaCertificatePath);
|
|
validationChain.ChainPolicy.CustomTrustStore.Add(ca);
|
|
validationChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Failed to load CA certificate from {Path}", tlsConfiguration.ServerCaCertificatePath);
|
|
}
|
|
}
|
|
|
|
if (tlsConfiguration.AllowSelfSignedCertificates)
|
|
{
|
|
validationChain.ChainPolicy.VerificationFlags |= X509VerificationFlags.AllowUnknownCertificateAuthority;
|
|
}
|
|
|
|
bool isValid = validationChain.Build(certificate2);
|
|
if (isValid)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (tlsConfiguration.AllowSelfSignedCertificates &&
|
|
validationChain.ChainStatus.All(status =>
|
|
status.Status == X509ChainStatusFlags.UntrustedRoot ||
|
|
status.Status == X509ChainStatusFlags.PartialChain))
|
|
{
|
|
logger.LogWarning("Accepting self-signed certificate for {Subject}", certificate2.Subject);
|
|
return true;
|
|
}
|
|
|
|
string statusMessage = string.Join(", ", validationChain.ChainStatus.Select(s => s.Status));
|
|
logger.LogWarning("Server certificate validation failed: {Status}", statusMessage);
|
|
return false;
|
|
}
|
|
|
|
private static X509Certificate2 LoadCertificate(string path)
|
|
{
|
|
try
|
|
{
|
|
return X509Certificate2.CreateFromPemFile(path);
|
|
}
|
|
catch
|
|
{
|
|
return new X509Certificate2(File.ReadAllBytes(path));
|
|
}
|
|
}
|
|
}
|