.NET Core 6 WCF客户端运行时加载自定义根CA验证SSL连接问题
我需要建立与SOAP服务的SSL连接,要求验证服务器证书的根CA不能存在于客户端系统根信任存储中,必须在运行时加载。最终部署在Linux环境,但Windows下也无法正常工作,仅当将根CA添加到系统CA存储时才能运行。该根CA为独立证书,无其他CA依赖,使用.NET Core 6开发,UserManagementClient为自动生成的客户端类:
public partial class UserManagementClient : System.ServiceModel.ClientBase<ConsoleApp7.UserManagementService.IUserManagement>, ConsoleApp7.UserManagementService.IUserManagement
尝试实现自定义证书验证器,但运行时从未被调用,程序抛出如下异常:
System.AggregateException: One or more errors occurred. (Could not establish trust relationship for the SSL/TLS secure channel with authority 'gui.net:8443'.)
---> System.ServiceModel.Security.SecurityNegotiationException: Could not establish trust relationship for the SSL/TLS secure channel with authority 'gui.net:8443'.
---> System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: UntrustedRoot
at System.Net.Security.SslStream.SendAuthResetSignal(ProtocolToken message, ExceptionDispatchInfo exception)
at System.Net.Security.SslStream.CompleteHandshake(SslAuthenticationOptions sslAuthenticationOptions)
at System.Net.Security.SslStream.ForceAuthenticationAsync[TIOAdapter](TIOAdapter adapter, Boolean receiveFirst, Byte[] reAuthenticationData, Boolean isApm)
at System.Net.Security.SslStream.ProcessAuthenticationWithTelemetryAsync(Boolean isAsync, Boolean isApm, CancellationToken cancellationToken)
at System.Net.Http.ConnectHelper.EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, Boolean async, Stream stream, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at System.Net.Http.ConnectHelper.EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, Boolean async, Stream stream, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.AddHttp11ConnectionAsync(HttpRequestMessage request)
at System.Threading.Tasks.TaskCompletionSourceWithCancellation1.WaitWithCancellationAsync(CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.GetHttp11ConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken) at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) at System.Net.Http.DecompressionHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken) at System.ServiceModel.Channels.HttpChannelFactory1.HttpClientRequestChannel.HttpClientChannelAsyncRequest.SendRequestAsync(Message message, TimeoutHelper timeoutHelper)
--- End of inner exception stack trace ---
at System.Runtime.AsyncResult.End[TAsyncResult](IAsyncResult result)
at System.ServiceModel.Channels.ServiceChannel.SendAsyncResult.End(SendAsyncResult result)
at System.ServiceModel.Channels.ServiceChannel.EndCall(String action, Object[] outs, IAsyncResult result)
at System.ServiceModel.Channels.ServiceChannelProxy.TaskCreator.<>c__DisplayClass1_0.<CreateGenericTask>b__0(IAsyncResult asyncResult)
使用的示例代码:
public void Test2(string url) { var httpBinding = new BasicHttpBinding(BasicHttpSecurityMode.None) { Name = "UserManagementServiceSoapBinding" }; httpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None; httpBinding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.UserName; if (uri.ToLower().Contains("https")) httpBinding.Security.Mode = BasicHttpSecurityMode.Transport; var endpointAddress = new EndpointAddress(uri); var client = new UserManagementClient(httpBinding, endpointAddress); client.ClientCredentials.ServiceCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.ChainTrust; client.ClientCredentials.ServiceCertificate.Authentication.CustomCertificateValidator = new CustomRootCertificateValidator(rootCaCertPath); try { var result = client.checkOTPAsync("aaa", new[] { "", "", "" }).Result; } catch (Exception e) { Console.WriteLine(e); return; } Console.WriteLine("Worked"); } public class CustomRootCertificateValidator : X509CertificateValidator { private X509Certificate2 _rootCertificate; public CustomRootCertificateValidator(string rootCertificatePath) { _rootCertificate = new X509Certificate2(rootCertificatePath); } public override void Validate(X509Certificate2 certificate) { if (certificate == null) { throw new ArgumentNullException(nameof(certificate)); } var chain = new X509Chain(); chain.ChainPolicy.ExtraStore.Add(_rootCertificate); chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; bool isValid = chain.Build(certificate); if (!isValid || chain.ChainElements[^1].Certificate.Thumbprint != _rootCertificate.Thumbprint) { throw new SecurityTokenValidationException("La validazione del certificato è fallita. Il certificato radice non è attendibile."); } } }
1. 修正证书验证模式配置
当前设置的CertificateValidationMode = X509CertificateValidationMode.ChainTrust会优先使用系统信任链验证,不会触发自定义验证器。必须将验证模式改为Custom才能启用自定义逻辑:
client.ClientCredentials.ServiceCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.Custom;
2. 绕过WCF底层HttpClient的默认验证逻辑
.NET Core的WCF实现依赖HttpClient建立连接,部分场景下WCF的证书验证器会被HttpClient的SSL验证覆盖。可以通过自定义HttpClientHandler的验证回调强制使用自定义根CA:
实现自定义EndpointBehavior注入Handler
// 加载自定义根CA var rootCa = new X509Certificate2(rootCaCertPath); // 创建自定义HttpClientHandler var handler = new HttpClientHandler(); handler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => { // 系统验证直接通过的情况 if (sslPolicyErrors == SslPolicyErrors.None) return true; // 仅处理UntrustedRoot错误 if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) != 0 && chain.ChainStatus.Any(s => s.Status == X509ChainStatusFlags.UntrustedRoot)) { var customChain = new X509Chain(); customChain.ChainPolicy.ExtraStore.Add(rootCa); customChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; // 验证证书链是否能通过自定义根CA完成 bool isValid = customChain.Build(cert); // 确保最终根CA是我们加载的目标证书 return isValid && customChain.ChainElements[^1].Certificate.Thumbprint.Equals(rootCa.Thumbprint, StringComparison.OrdinalIgnoreCase); } // 其他错误直接拒绝 return false; }; // 创建绑定并配置安全模式 var httpBinding = new BasicHttpBinding(BasicHttpSecurityMode.Transport); httpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None; httpBinding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.UserName; var endpointAddress = new EndpointAddress(uri); var client = new UserManagementClient(httpBinding, endpointAddress); // 注入自定义Handler到WCF客户端 client.Endpoint.EndpointBehaviors.Add(new CustomHttpClientHandlerBehavior(handler));
对应的CustomHttpClientHandlerBehavior实现:
public class CustomHttpClientHandlerBehavior : IEndpointBehavior { private readonly HttpClientHandler _handler; public CustomHttpClientHandlerBehavior(HttpClientHandler handler) { _handler = handler; } public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { bindingParameters.Add(_handler); } public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { } public void Validate(ServiceEndpoint endpoint) { } }
3. 确保根CA证书加载正确
- Linux环境下注意证书文件的读取权限,避免因权限不足无法加载
- 如果证书带密码,加载时需指定:
new X509Certificate2(rootCaCertPath, "证书密码") - 可在代码中输出根CA的Thumbprint,与服务器证书链的根证书Thumbprint对比,确认是否匹配
4. 检查服务器证书有效性
- 确认服务器证书的
Subject Alternative Name包含服务域名gui.net - 检查服务器证书是否过期,或存在其他链问题(因根CA独立,中间CA缺失问题可忽略)
内容的提问来源于stack exchange,提问作者Gnagno

