feat(lmxproxy): phase 1 — v2 protocol types and domain model
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user