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); } /// /// Creates a gRPC channel with optional TLS configuration. /// /// The server address. /// Optional TLS configuration. /// The logger. /// A configured gRPC channel. 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)); } } }