WCF 下的 SOAP Web 服务的同时客户端证书和用户名身份验证

Simultaneous client-certificate and username authentication of a SOAP web service under WCF

我无法通过 SOAP Web 服务进行身份验证,这需要使用客户端证书和用户名同时进行身份验证。

目标服务基本上支持三种身份验证模式:

在每种情况下,都是使用 TLS 1.2 的 HTTPS。

事实上,我不知道如何配置绑定和身份验证设置来描述 WCF 中的这种身份验证模式。到目前为止,我尝试了各种 BasicHttpsBinding配置和 CustomBinding 设置,包括 this answer.

中描述的设置

在下面的代码中,当同时指定 endpointConfig.UsernameendpointConfig.ClientCertificate 时,我根据能够 google 的示例构建了一个 CustomBinding,但它赢了工作。客户端确实向服务器发送了正确的证书,服务器用 Change Cipher Spec 消息回复,客户端终止了 TLS 连接。

我最多能得到以下错误信息:

Could not establish trust relationship for the SSL/TLS secure channel with authority 'ws1c.czebox.cz'.

Could not establish secure channel for SSL/TLS with authority 'ws1c.czebox.cz'.

构建服务客户端的代码如下所示:

// prepare binding
Binding binding;
// NOTE A username alone is sufficient for Basic authentication e.g. when logging-in to a DataBox using the HostCert method
if (!String.IsNullOrEmpty(endpointConfig.Username) && endpointConfig.ClientCertificate != null)
{
    // client certificate and username + password
    // the below code is experimental and just doesn't work (yields the first error mentioned above)
    var tb = new HttpsTransportBindingElement();
    tb.MaxReceivedMessageSize = MaxSupportedMessageSize;
    tb.RequireClientCertificate = true;
    tb.AuthenticationScheme = AuthenticationSchemes.Basic;
    var ub = SecurityBindingElement.CreateUserNameOverTransportBindingElement();
    binding = new CustomBinding(ub, tb);
}
else if (!String.IsNullOrEmpty(endpointConfig.Username))
{
    // username + password
    var b = new BasicHttpsBinding(BasicHttpsSecurityMode.Transport);
    b.MaxReceivedMessageSize = MaxSupportedMessageSize;
    b.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;
    binding = b;
}
else if (endpointConfig.ClientCertificate != null)
{
    // client certificate
    var b = new BasicHttpsBinding(BasicHttpsSecurityMode.Transport);
    b.MaxReceivedMessageSize = MaxSupportedMessageSize;
    b.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
    binding = b;
}
else
{
    throw new NotSupportedException();
}

// prepare endpoint identity
EndpointIdentity identity = null;
X509Certificate2 cert = null;
if (endpointConfig.ClientCertificate != null)
{
    var clientCredentials = new ClientCredentials();
    clientCredentials.ClientCertificate.SetCertificate(
        endpointConfig.ClientCertificate.StoreLocation,
        endpointConfig.ClientCertificate.StoreName,
        endpointConfig.ClientCertificate.FindBy,
        endpointConfig.ClientCertificate.FindValue);
    cert = clientCredentials.ClientCertificate.Certificate;
    identity = EndpointIdentity.CreateX509CertificateIdentity(cert);
}

// prepare endpoint address
var address = new EndpointAddress(new Uri(endpointConfig.EndpointUrl), identity);

// create connector 
var connector = new dmInfoPortTypeClient(binding, address);

// setup username + password authentication
// NOTE Password may be empty when logging-in to a DataBox using the HostCert method
if (!String.IsNullOrEmpty(endpointConfig.Username))
{
    connector.ClientCredentials.UserName.UserName = endpointConfig.Username;
    connector.ClientCredentials.UserName.Password = endpointConfig.Password;
}

// setup client-certificate authentication
if (endpointConfig.ClientCertificate != null)
{
    connector.ClientCredentials.ClientCertificate.Certificate = cert;
}

请注意,我对服务器的控制为零。是政府服务,没法商量。

运行 在 .NET 4.7.x 上。可以升级到 4.8,但不能迁移到 .NET Core。

(根据广泛的调试和诊断得出的解决方案进行自我回答。欢迎提供更好的答案。)

原来情况和this Q&A pair描述的差不多。但是,在我的例子中,我以编程方式构建 WCF 端点,避免了 XML 配置。

1) 正确的自定义绑定

对于证书 + 用户名选项,以下代码构造了一个有效的自定义绑定:

// HTTPS transport with basic authentication and required client certificate
// note that AuthenticationSchemes.Certificate doesn't work, the server requires
// Basic authentication
var tb = new HttpsTransportBindingElement();
tb.MaxReceivedMessageSize = MaxSupportedMessageSize;
tb.RequireClientCertificate = true;
tb.AuthenticationScheme = AuthenticationSchemes.Basic;

// encoding: the server uses SOAP 1.1. which results in the MIME content-type text/xml
// note that by default, the encoding is SOAP 1.2, which implies the content-type
// application/soap+xml not compatible with the server, resulting in an exception
var enc = new TextMessageEncodingBindingElement(MessageVersion.Soap11, Encoding.UTF8);

// transport security: pass username over transport; it is necessary to allow
// unsecured responses; don't know why, but without this setting it won't work
var ub = SecurityBindingElement.CreateUserNameOverTransportBindingElement();
ub.EnableUnsecuredResponse = true;

// construct the binding
binding = new CustomBinding(enc, ub, tb);

注意:这是两个方面,IMO,根据我的具体情况定制,因此不适合通用解决方案:

  • 使用显式编码表示 MessageVersion.Soap11 用于 SOAP 1.1;默认情况下,SOAP 1.2 的编码是 MessageVersion.Soap12
  • 使用 SecurityBindingElement.EnableUnsecuredResponse
  • 启用不安全响应

2) 没有端点身份

// prepare endpoint identity 注释的代码部分是错误的,错误地使用 客户端 证书作为 WCF 应该期望从 服务器.

3) WCF 跟踪

诊断问题时,启用 WCF 跟踪的功能很有帮助。可以在 app.config 中使用以下内容来启用日志记录:

<system.diagnostics>
    <!-- use for WCF diagnostis: WCF tracing and message logging -->
    <sources>
        <source name="System.ServiceModel"
        switchValue="Information, ActivityTracing"
        propagateActivity="true">
        <listeners>
            <add name="traceListener"
            type="System.Diagnostics.XmlWriterTraceListener"
            initializeData= "D:\Logs\WCF-Traces.svclog" />
        </listeners>
        </source>
        <source name="System.ServiceModel.MessageLogging">
        <listeners>
            <add name="messages"
            type="System.Diagnostics.XmlWriterTraceListener"
            initializeData="D:\Logs\WCF-Messages.svclog" />
        </listeners>
        </source>
    </sources>
</system.diagnostics>