WCF SOAP 1.1 and WS-Security 1.0, client certifica

2019-01-21 11:55发布

问题:

Summary: I am working on a .NET 4.0 WCF client to consume a web service (DataPower, Java service on the other end) using SOAP 1.1 and WS-Security 1.0. The WCF client must implement a client certificate for mutual authentication at the transport layer. The message body needs to be signed using a separate service/signing certificate. The SOAP header also needs to contain a Username Token with Password Digest and include Nonce and Created tags.

I am able to consume this web service using WSE 3.0 with BasicHTTPBinding. But I have not been successful so far in implementing the same with WCF using either WSHttpBinding or CustomBinding. I’ve tried all of the security binding elements and have had no luck so far.

I am also using the usernametoken library from here (http://blogs.msdn.com/b/aszego/archive/2010/06/24/usernametoken-profile-vs-wcf.aspx) so I can add password digest/nonce/created in the UsernameToken in SOAP header.

I am currently using SecurityBindingElement.CreateMutualCertificateBindingElement I’ve also tried several others such as AsymmetricSecurityBindingElement, TransportSecurityBindingElement etc (commented out in code below)

CERTS: I have both the client certificate and service certificate loaded into the certificate store using MMC (I am on Windows 7 btw.) Both the client cert and the service cert have private keys. I’ve loaded both the PFX files into LocalMachine/Personal, LocalMachine/Root and LocalMachine/TrustedPeople. I have also run the FindPrivateKey/ICACLS to give permission to “IIS App Pool/DefaultAppPool” account. Although none of this should matter since I can run the WSE 3.0 code from my machine and it works without any cert issues.

Commands run:

FindPrivateKey.exe My LocalMachine -t "thumbprint of client cert"
FindPrivateKey.exe My LocalMachine -t "thumbprint of service cert"
icacls C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\{privateKeyOfClientCert} /grant "IIS AppPool\DefaultAppPool":R      <<Successfully processed 1 files; Failed processing 0 files>>
icacls C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\{privateKeyOfServiceCert} /grant "IIS AppPool\DefaultAppPool":R     <<Successfully processed 1 files; Failed processing 0 files>>

WCF ISSUE: I currently receive a “Could not establish secure channel for SSL/TLS with authority 'x.x.com'” message back from the DataPower gateway. I figure this might be because the gateway is taking the service certificate and using that for client authenticate instead of using the client certificate I am sending. I am saying this because when I do not specify the DNS Identity for the endpoint, I get back a message saying that the gateway is expecting the DNS identity to be “{subject name of service/signing certificate}”.

Here is the SOAP request generated by WCF which is giving the above error. The WCF SOAP request looks very similar to the WSE SOAP request. The above error is most probably occurring due to the cert issue at the SSL/Transport layer.

WCF SOAP request:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
    <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
        <u:Timestamp u:Id="uuid-8533d9a5-865e-4a4b-a750-fadb7c1ce36c-1">
            <u:Created>2013-02-06T20:53:04.679Z</u:Created>
            <u:Expires>2013-02-06T20:58:04.679Z</u:Expires>
        </u:Timestamp>
        <o:BinarySecurityToken u:Id="uuid-0bab08ce-3e3b-4360-a44b-694b06a3dd67-2" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3">Removed Service Cert Encoded Value</o:BinarySecurityToken>
        <wsse:UsernameToken wsu:Id="7843ab92-f69a-4d00-a5ba-117e32a74f49" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
                            xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
            <wsse:Username>USER_Removed</wsse:Username>
            <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">XXX=</wsse:Password>
            <wsse:Nonce>XXX==</wsse:Nonce>
            <wsu:Created>2013-02-06T20:53:04Z</wsu:Created>
        </wsse:UsernameToken>
        <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
            <SignedInfo>
                <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></CanonicalizationMethod>
                <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"></SignatureMethod>
                <Reference URI="#_1">
                    <Transforms>
                        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></Transform>
                    </Transforms>
                    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod>
                    <DigestValue>XXX=</DigestValue>
                </Reference>
                <Reference URI="#uuid-8533d9a5-865e-4a4b-a750-fadb7c1ce36c-1">
                    <Transforms>
                        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></Transform>
                    </Transforms>
                    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod>
                    <DigestValue>XXX=</DigestValue>
                </Reference>
                <Reference URI="#7843ab92-f69a-4d00-a5ba-117e32a74f49">
                    <Transforms>
                        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></Transform>
                    </Transforms>
                    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod>
                    <DigestValue>XXX=</DigestValue>
                </Reference>
            </SignedInfo>
            <SignatureValue>XXXLongXXX=</SignatureValue>
            <KeyInfo>
                <o:SecurityTokenReference>
                    <o:Reference URI="#uuid-0bab08ce-3e3b-4360-a44b-694b06a3dd67-2"></o:Reference>
                </o:SecurityTokenReference>
            </KeyInfo>
        </Signature>
    </o:Security>
</s:Header>
<s:Body u:Id="_1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <ping xmlns="https://x.x.com/xxx/v1">
        <pingRequest xmlns="">hello</pingRequest>
    </ping>
</s:Body>

WSE 3.0 SOAP request (this works):

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
           xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<soap:Header>
    <wsa:Action wsu:Id="Id-4271fb72-464a-467d-ab1f-4d32542e20f0"/>
    <wsa:MessageID wsu:Id="Id-11657f64-d856-47d8-b600-d5379fb91a0d">urn:uuid:ff8becb7-74c2-4844-ab46-8ae23f1355a7</wsa:MessageID>
    <wsa:ReplyTo wsu:Id="Id-40b2e6e8-e67b-4a6c-a545-071ce0f0107a">
        <wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
    </wsa:ReplyTo>
    <wsa:To wsu:Id="Id-d5e0b488-6f8a-479c-940d-2b85833dbc66">https://x.x.com/xxx/v1</wsa:To>
    <wsse:Security soap:mustUnderstand="1">
        <wsu:Timestamp wsu:Id="Timestamp-68476551-5c58-4a47-967b-54ec18257b1b">
            <wsu:Created>2013-02-06T19:38:39Z</wsu:Created>
            <wsu:Expires>2013-02-06T19:43:39Z</wsu:Expires>
        </wsu:Timestamp>
        <wsse:UsernameToken wsu:Id="SecurityToken-e5f65166-a825-48cb-a939-8e515a637e01">
            <wsse:Username>USER_Removed</wsse:Username>
            <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">XXX=</wsse:Password>
            <wsse:Nonce>XXX==</wsse:Nonce>
            <wsu:Created>2013-02-06T19:38:39Z</wsu:Created>
        </wsse:UsernameToken>
        <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
            <SignedInfo>
                <ds:CanonicalizationMethod xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
                <Reference URI="#Id-4271fb72-464a-467d-ab1f-4d32542e20f0">
                    <Transforms>
                        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                    </Transforms>
                    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                    <DigestValue>XXX=</DigestValue>
                </Reference>
                <Reference URI="#Id-11657f64-d856-47d8-b600-d5379fb91a0d">
                    <Transforms>
                        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                    </Transforms>
                    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                    <DigestValue>XXX=</DigestValue>
                </Reference>
                <Reference URI="#Id-40b2e6e8-e67b-4a6c-a545-071ce0f0107a">
                    <Transforms>
                        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                    </Transforms>
                    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                    <DigestValue>XXX=</DigestValue>
                </Reference>
                <Reference URI="#Id-d5e0b488-6f8a-479c-940d-2b85833dbc66">
                    <Transforms>
                        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                    </Transforms>
                    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                    <DigestValue>XXX=</DigestValue>
                </Reference>
                <Reference URI="#Timestamp-68476551-5c58-4a47-967b-54ec18257b1b">
                    <Transforms>
                        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                    </Transforms>
                    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                    <DigestValue>XXX=</DigestValue>
                </Reference>
                <Reference URI="#Id-6f76e50e-932c-4878-bbc0-3ef4c8a36990">
                    <Transforms>
                        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                    </Transforms>
                    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                    <DigestValue>XXX=</DigestValue>
                </Reference>
            </SignedInfo>
            <SignatureValue>XXXLongXXX=</SignatureValue>
            <KeyInfo>
                <wsse:SecurityTokenReference>
                    <wsse:KeyIdentifier ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509SubjectKeyIdentifier"
                                        EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">XXX=</wsse:KeyIdentifier>
                </wsse:SecurityTokenReference>
            </KeyInfo>
        </Signature>
    </wsse:Security>
</soap:Header>
<soap:Body wsu:Id="Id-6f76e50e-932c-4878-bbc0-3ef4c8a36990">
    <ping xmlns="https://x.x.com/xxx/v1">
        <pingRequest xmlns="">hello</pingRequest>
    </ping>
</soap:Body>

Here is all of the config, please let me know what I am doing wrong!

WCF web.config: I removed everything from the web.config as I am doing all the config in code.

WCF config in code:

var proxy = GetProxy();
pingResponseMessage resp = proxy.ping("hello");
lblStatus.Text = resp.status.ToString();

private XXXClient GetProxy()
{

    System.Net.ServicePointManager.ServerCertificateValidationCallback += (se, cert, chain, sslerror) => { return true; };

    XXXClient proxy = new XXXClient(GetCustomBinding(), new EndpointAddress(new Uri("https://xxx"), EndpointIdentity.CreateDnsIdentity("I am forced to put the signing cert subject here, nothing else works"), new AddressHeaderCollection()));

    proxy.Endpoint.Behaviors.Remove(typeof(ClientCredentials));
    proxy.Endpoint.Behaviors.Add(new UsernameClientCredentials(new UsernameInfo(@"USER_Removed", "X")));

    proxy.ClientCredentials.ClientCertificate.SetCertificate(StoreLocation.LocalMachine, StoreName.My, X509FindType.FindByThumbprint, "REMOVED");
    proxy.ClientCredentials.ServiceCertificate.SetDefaultCertificate(StoreLocation.LocalMachine, StoreName.My, X509FindType.FindByThumbprint, "REMOVED");
    proxy.ClientCredentials.ServiceCertificate.Authentication.CertificateValidationMode = System.ServiceModel.Security.X509CertificateValidationMode.None;

    return proxy;
}

private Binding GetCustomBinding()
{
    //TransportSecurityBindingElement secBE = SecurityBindingElement.CreateCertificateOverTransportBindingElement(MessageSecurityVersion.WSSecurity10WSTrust13WSSecureConversation13WSSecurityPolicy12BasicSecurityProfile10);
    //AsymmetricSecurityBindingElement secBE = (AsymmetricSecurityBindingElement)SecurityBindingElement.CreateMutualCertificateBindingElement(MessageSecurityVersion.WSSecurity10WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11BasicSecurityProfile10);
    //secBE.InitiatorTokenParameters = new System.ServiceModel.Security.Tokens.X509SecurityTokenParameters { InclusionMode = SecurityTokenInclusionMode.AlwaysToRecipient, RequireDerivedKeys = false, X509ReferenceStyle = X509KeyIdentifierClauseType.SubjectKeyIdentifier };
    //secBE.RecipientTokenParameters = new System.ServiceModel.Security.Tokens.X509SecurityTokenParameters { InclusionMode = SecurityTokenInclusionMode.AlwaysToInitiator, RequireDerivedKeys = false, X509ReferenceStyle = X509KeyIdentifierClauseType.SubjectKeyIdentifier };
    //secBE.MessageProtectionOrder = System.ServiceModel.Security.MessageProtectionOrder.SignBeforeEncrypt;
    //secBE.EndpointSupportingTokenParameters.Signed.Add(new UserNameSecurityTokenParameters() { InclusionMode = SecurityTokenInclusionMode.AlwaysToRecipient, RequireDerivedKeys = false });
    //secBE.EndpointSupportingTokenParameters.Signed.Add(new X509SecurityTokenParameters(X509KeyIdentifierClauseType.SubjectKeyIdentifier, SecurityTokenInclusionMode.Never) { InclusionMode = SecurityTokenInclusionMode.Never, RequireDerivedKeys = false, X509ReferenceStyle = X509KeyIdentifierClauseType.SubjectKeyIdentifier });
    //secBE.ProtectionTokenParameters = new System.ServiceModel.Security.Tokens.X509SecurityTokenParameters { InclusionMode = SecurityTokenInclusionMode.AlwaysToRecipient };
    //secBE.DefaultAlgorithmSuite = new CustomSecurityAlgorithm();

    SecurityBindingElement secBE = SecurityBindingElement.CreateMutualCertificateBindingElement(MessageSecurityVersion.WSSecurity10WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11BasicSecurityProfile10);
    secBE.MessageSecurityVersion = MessageSecurityVersion.WSSecurity10WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11BasicSecurityProfile10;
    secBE.EndpointSupportingTokenParameters.Signed.Add(new UsernameTokenParameters() { InclusionMode= SecurityTokenInclusionMode.AlwaysToRecipient, ReferenceStyle = SecurityTokenReferenceStyle.External, RequireDerivedKeys = false });
    secBE.SecurityHeaderLayout = SecurityHeaderLayout.Strict;
    //secBE.AllowInsecureTransport = false;
    //secBE.AllowSerializedSigningTokenOnReply = false;
    secBE.EnableUnsecuredResponse = true;
   secBE.IncludeTimestamp = true;
    secBE.SetKeyDerivation(false);

    TextMessageEncodingBindingElement textEncBE = new TextMessageEncodingBindingElement(MessageVersion.Soap11, System.Text.Encoding.UTF8);

    HttpsTransportBindingElement httpsBE = new HttpsTransportBindingElement();
    httpsBE.RequireClientCertificate = true;
    //httpsBindingElement.AllowCookies = false;
    //httpsBindingElement.AuthenticationScheme = System.Net.AuthenticationSchemes.Basic;
    httpsBE.BypassProxyOnLocal = false;
    httpsBE.HostNameComparisonMode = HostNameComparisonMode.StrongWildcard;
    //httpsBindingElement.KeepAliveEnabled = false;
    httpsBE.TransferMode = TransferMode.Buffered;
    httpsBE.UseDefaultWebProxy = true;

    CustomBinding myBinding = new CustomBinding();
    myBinding.Elements.Add(secBE);
    myBinding.Elements.Add(textEncBE);
    myBinding.Elements.Add(httpsBE);

    return myBinding;
}

I’ve added ProtectionLevel.Sign on the ServiceContract and OperationContracts since I only need to sign the message body. I haven’t gotten this far to verify it yet though.

[System.ServiceModel.ServiceContractAttribute(Namespace = "https://x.x.com/xxx/v1", ConfigurationName = "x.x", ProtectionLevel = System.Net.Security.ProtectionLevel.Sign)]
public interface XXXService {
    [System.ServiceModel.OperationContractAttribute(Action = "", ReplyAction = "*", ProtectionLevel = System.Net.Security.ProtectionLevel.Sign)]
    [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)]
    [return: System.ServiceModel.MessageParameterAttribute(Name="return")]
    XXX.pingResponse ping(XXX.ping request);

[System.ServiceModel.ServiceContractAttribute(Namespace = "https://x.x.com/xxx/v1", ProtectionLevel = System.Net.Security.ProtectionLevel.Sign)]
public partial class XXXClient : System.ServiceModel.ClientBase<XXXService> {
    [System.ServiceModel.OperationContractAttribute(Action = "", ReplyAction = "*", ProtectionLevel = System.Net.Security.ProtectionLevel.Sign)]
    public XXX.pingResponseMessage ping(string pingRequest) {

I’ve added the following the below to web.config to allow logging of the entire soap including pii data

(for pii, also added <machineSettings enableLoggingKnownPii="true" /> under <system.serviceModel> to C:\Windows\Microsoft.NET\Framework\vX\CONFIG\machine.config)

<system.serviceModel>
<diagnostics>
  <messageLogging logKnownPii="true" logEntireMessage="true" logMalformedMessages="true" logMessagesAtServiceLevel="true" logMessagesAtTransportLevel="true" maxMessagesToLog="3000"/>
</diagnostics>
</system.serviceModel>
<system.diagnostics>
<sources>
  <source name="System.ServiceModel.MessageLogging" logKnownPii="true">
    <listeners>
      <add initializeData="C:\trace.log" type="System.Diagnostics.XmlWriterTraceListener" name="messages"/>
    </listeners>
  </source>
</sources>
</system.diagnostics>

===============

WSE 3.0 (Working config and code): web.config:

<system.serviceModel>
<bindings>
  <basicHttpBinding>
    <binding name="myBinding" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00" allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard" maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536" messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered" useDefaultWebProxy="true">
      <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384"/>
      <security mode="Transport">
        <transport clientCredentialType="None" proxyCredentialType="None" realm=""/>
        <message clientCredentialType="UserName" algorithmSuite="Default"/>
      </security>
    </binding>
  </basicHttpBinding>
</bindings>
<client>
  <endpoint address="https://x.x.com/xxx/v1" binding="basicHttpBinding" bindingConfiguration="myBinding" contract="XXXService" name="XXX"/>
</client>
</system.serviceModel>
<appSettings>
<add key="XXXImplService" value="https://x.x.com/xxx/v1"/>
</appSettings>

... and WSE3 code:

var proxy = new XXXImplServiceWse();

UsernameToken usernameToken = new UsernameToken(@"USER_Removed", "X");
proxy.RequestSoapContext.Security.Tokens.Add(usernameToken);

X509Certificate2 mutualCert = LoadCertFromStore(StoreLocation.LocalMachine, StoreName.My, "Client Cert Subject Name");
proxy.ClientCertificates.Add(mutualCert);

X509Certificate2 signCert = LoadCertFromStore(StoreLocation.LocalMachine, StoreName.My, "Service Cert Subject Name");

X509SecurityToken signatureToken = new X509SecurityToken(signCert);

MessageSignature signature = new MessageSignature(signatureToken); // <!-- IS THIS SAME AS THIS STEP IN WCF: secBE.EndpointSupportingTokenParameters.Signed.Add(new UsernameTokenParameters()) -->

proxy.RequestSoapContext.Security.Elements.Add(signature);

==========

So, how do I convert the above WSE 3.0 code to WCF?

回答1:

I was able to resolve my issue and connect to DataPower (IBM Xi50) Web Service Gateway using the following WCF CustomBinding (CertificateOverTransport) and CustomCredentials (UsernameToken with Password Digest, client cert for transport authentication and service cert for message body signature.) I am not sure what exactly fixed the issue, but here is my working WCF code! I hope this helps others who are in a similar situation as I was.

Please verify that the DataPower Xi50 gateway is also configured for WCF. From IBM: "When using BasicHttpBinding with SSL: You may use disable-ssl-cipher-check parameter to disable cipher checks for any TransportBinding assertions. The Basic Auth Header is not supported by default in the Web services proxy. A custom configuration of an on-error rule to inject the WWW-Authenticate header is required to inter-op with WCF." For details, go here: https://publib.boulder.ibm.com/infocenter/ieduasst/v1r1m0/index.jsp?topic=/com.ibm.iea.wdatapower/wdatapower/1.0/xa35/380DataPowerWCFIntegration/player.html.

Make sure you have set ProtectionLevel.Sign on your service contract if you want your message body signed only (and not Encrypted.)

For DNS Identity, which I had issues with earlier, I was now able to put my client certificate subject name - earlier this wouldn't work.

I don't have any configuration in my web.config.

Here is the Proxy using CustomBinding:

private ClientProxy GetProxy()
{
    XXXServiceClient proxy = new XXXServiceClient(GetCustomBinding(), new EndpointAddress(new Uri("<<GatewayURLHere>>"), EndpointIdentity.CreateDnsIdentity("<<DNS or Client Cert Subject Name>>"), new AddressHeaderCollection()));
    proxy.Endpoint.Behaviors.Remove(typeof(ClientCredentials));
    proxy.Endpoint.Behaviors.Add(new CustomCredentials(<clientCertHere>, <signingCertHere>));
    proxy.ClientCredentials.UserName.UserName = @"XXX";
    proxy.ClientCredentials.UserName.Password = "yyy";
    return proxy;
}

private Binding GetCustomBinding()
{
    TransportSecurityBindingElement secBE = SecurityBindingElement.CreateCertificateOverTransportBindingElement(MessageSecurityVersion.WSSecurity10WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11BasicSecurityProfile10);
    secBE.EndpointSupportingTokenParameters.Signed.Add(new UserNameSecurityTokenParameters { InclusionMode = SecurityTokenInclusionMode.Never, RequireDerivedKeys = false });
    secBE.EnableUnsecuredResponse = true;
    secBE.IncludeTimestamp = true;
    TextMessageEncodingBindingElement textEncBE = new TextMessageEncodingBindingElement(MessageVersion.Soap11WSAddressingAugust2004, System.Text.Encoding.UTF8);
    HttpsTransportBindingElement httpsBE = new HttpsTransportBindingElement();
    httpsBE.RequireClientCertificate = true;

    CustomBinding myBinding = new CustomBinding();
    myBinding.Elements.Add(secBE);
    myBinding.Elements.Add(textEncBE);
    myBinding.Elements.Add(httpsBE);

    return myBinding;
}

Here is my CustomCredentials class that I put together from multiple sources including the above mentioned UsernameToken library - sets client certificate for (mutual?) authentication at the transport layer, service/signing certificate for signing the message body and UsernameToken with Password Digest in the SOAP header:

using System;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Security;
using System.Text;

namespace XXX_WCF
{
    public class CustomCredentials : ClientCredentials
    {
        private X509Certificate2 clientAuthCert;
        private X509Certificate2 clientSigningCert;

        public CustomCredentials() : base() { }

        public CustomCredentials(CustomCredentials other)
            : base(other)
        {
            clientSigningCert = other.clientSigningCert;
            clientAuthCert = other.clientAuthCert;
        }

        protected override ClientCredentials CloneCore()
        {
            CustomCredentials scc = new CustomCredentials(this);
            return scc;
        }

        public CustomCredentials(X509Certificate2 ClientAuthCert, X509Certificate2 ClientSigningCert)
            : base()
        {
            clientAuthCert = ClientAuthCert;
            clientSigningCert = ClientSigningCert;
        }

        public X509Certificate2 ClientAuthCert
        {
            get { return clientAuthCert; }
            set { clientAuthCert = value; }
        }

        public X509Certificate2 ClientSigningCert
        {
            get { return clientSigningCert; }
            set { clientSigningCert = value; }
        }

        public override SecurityTokenManager CreateSecurityTokenManager()
        {
            return new CustomTokenManager(this);
        }
    }

    public class CustomTokenManager : ClientCredentialsSecurityTokenManager
    {
        private CustomCredentials custCreds;

        public CustomTokenManager(CustomCredentials CustCreds)
            : base(CustCreds)
        {
            custCreds = CustCreds;
        }

        public override SecurityTokenProvider CreateSecurityTokenProvider(SecurityTokenRequirement tokenRequirement)
        {
            if (tokenRequirement.TokenType == SecurityTokenTypes.X509Certificate)
            {
                x509CustomSecurityTokenProvider prov;
                object temp = null;
                TransportSecurityBindingElement secBE = null;

                if (tokenRequirement.Properties.TryGetValue("http://schemas.microsoft.com/ws/2006/05/servicemodel/securitytokenrequirement/SecurityBindingElement", out temp))
                {
                    secBE = (TransportSecurityBindingElement)temp;
                }

                if (secBE == null)
                    prov = new x509CustomSecurityTokenProvider(custCreds.ClientAuthCert);
                else
                    prov = new x509CustomSecurityTokenProvider(custCreds.ClientSigningCert);
                return prov;
            }

            return base.CreateSecurityTokenProvider(tokenRequirement);
        }

        public override System.IdentityModel.Selectors.SecurityTokenSerializer CreateSecurityTokenSerializer(System.IdentityModel.Selectors.SecurityTokenVersion version)
        {
            return new CustomTokenSerializer(System.ServiceModel.Security.SecurityVersion.WSSecurity10);
        }
    }

    class x509CustomSecurityTokenProvider : SecurityTokenProvider
    {
        private X509Certificate2 clientCert;

        public x509CustomSecurityTokenProvider(X509Certificate2 cert)
            : base()
        {
            clientCert = cert;
        }

        protected override SecurityToken GetTokenCore(TimeSpan timeout)
        {
            return new X509SecurityToken(clientCert);
        }
    }

    public class CustomTokenSerializer : WSSecurityTokenSerializer
    {
        public CustomTokenSerializer(SecurityVersion sv) : base(sv) { }

        protected override void WriteTokenCore(System.Xml.XmlWriter writer, System.IdentityModel.Tokens.SecurityToken token)
        {
            if (writer == null)
            {
                throw new ArgumentNullException("writer");
            }
            if (token == null)
            {
                throw new ArgumentNullException("token");
            }

            if (token.GetType() == new UserNameSecurityToken("x", "y").GetType())
            {
                UserNameSecurityToken userToken = token as UserNameSecurityToken;

                if (userToken == null)
                {
                    throw new ArgumentNullException("userToken: " + token.ToString());
                }

                string tokennamespace = "o";

                DateTime created = DateTime.Now;
                string createdStr = created.ToString("yyyy-MM-ddThh:mm:ss.fffZ");
                string phrase = Guid.NewGuid().ToString();
                string nonce = GetSHA1String(phrase);
                string password = GetSHA1String(nonce + createdStr + userToken.Password);
                //string password = userToken.Password;

                writer.WriteStartElement(tokennamespace, "UsernameToken", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
                writer.WriteAttributeString("u", "Id", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", token.Id);
                writer.WriteElementString(tokennamespace, "Username", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", userToken.UserName);
                writer.WriteStartElement(tokennamespace, "Password", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
                writer.WriteAttributeString("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest");
                writer.WriteValue(password);
                writer.WriteEndElement();
                writer.WriteStartElement(tokennamespace, "Nonce", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
                writer.WriteAttributeString("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary");
                writer.WriteValue(nonce);
                writer.WriteEndElement();
                writer.WriteElementString(tokennamespace, "Created", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", createdStr);
                writer.WriteEndElement();
                writer.Flush();
            }
            else
            {
                base.WriteTokenCore(writer, token);
            }
        }

        protected string GetSHA1String(string phrase)
        {
            SHA1CryptoServiceProvider sha1Hasher = new SHA1CryptoServiceProvider();
            byte[] hashedDataBytes = sha1Hasher.ComputeHash(Encoding.UTF8.GetBytes(phrase));
            return Convert.ToBase64String(hashedDataBytes);
        }
    }//CustomTokenSerializer
}

Good luck!



回答2:

I went through your code and it looks correct. There is small difference in WSE and WCF soap message but the difference is only in the way how the certificate used to sign the message is referenced.

I think the core issue here is wrong usage of certificates. You are using both transport and message mutual security. In theory this requires four certificates. You need

  • Service certificate for transport security - this certificate is used by server to build SSL connection. To successfully build the connection client must trust the certificate (you either need to trust authority which issued the certificate or the server certificate must be placed in your trusted people store).
  • Client certificate for transport security - this certificate is used to authenticate client on the server at transport level - you must have certificate and its private key in your personal store
  • Service certificate for message security - this certificate is used for encrypting request and signing response (when WS-Security 1.0) is used. You need to have this certificate somewhere on your machine (it is up to you what location will be used to load the certificate).
  • Client certificate for message security - this certificate is used for encrypting response and signing request (when WS-Security 1.0) is used. You need to have this certificate and its private key somewhere on your machine (it is up to you what location will be used to load the certificate).

It looks like you have only two certificates - one client and one server. In such case they should probably be used for both transport and message security. But here comes interesting issue - your "signing" certificate on the client side in WSE example is actually service certificate. If it is really the case, it means that client must have access to server's private key - that should never ever happen. That is the worst violation of PKI infrastructure. PKI infrastructure is based on trust to certificate authorities and on securing private keys where each participant has its own private key not accessible by anyone else. Sharing private keys reduces security. In the worst case it can be equal to no security at all because anyone with access to private key can intercept the communication or fake signature on the message.

If I'm right you should use WSE 3.0 and be happy with that. Just forcing WCF to use different client certificate for HTTPS and message security can be quite hard. You have single ClientCertificate property but you need to load different certificate for HTTPS and message security. It requires creating custom ClientCredentials with two properties and custom SecurityTokenManager to return correct certificate provider (by implementing for each usage (that is a theory - I have never tried it).

Btw. your problem with EndpointIdentity is based on the fact that your service is exposed on some DNS and if the subject in the service certificate (which is in your case also signing certificate) is different you must create a new DNS identity for your endpoint. Otherwise WCF will not trust the certificate. Server certificate should be issued with subject matching DNS name used to access the server.