You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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.&lt;SendAsync&gt;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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 23:44:55