How To Sign WCF Message With Certificate Using HTTPS Transport with Client Certificate

偶尔善良 提交于 2019-12-05 00:13:18



I finally determined that IClientMessageInspector does not seem to reflect message signing so when I was actually getting a signature in my request I didn't know it. So now for my new, real question...

How can I configure a WCF client to present both the SSL client cert and sign the SOAP Header?

var myBinding = new BasicHttpBinding();
myBinding.Security.Mode = BasicHttpSecurityMode.TransportWithMessageCredential;
myBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
myBinding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.Certificate;

This will result in the header having a timestamp that's signed. However, the client cert is no longer presented and I don't get past SSL. If I change the second line to:

myBinding.Security.Mode = BasicHttpSecurityMode.Transport;

Then I get past SSL but my SOAP header no longer has the signing block.

Is there any way I can get ahold of HttpWebRequest so that I can manually attach the SSL Client cert like so?


Original Question

I am working on a WCF client that needs to interop with a service provided by a third party that's using a Forum Sentry network appliance to secure access. They require SSL with a client cert at the transport level as well as an o:Security element with signature using an cert in the header. I'm able to achieve one or othe other with standard bindings, but I can't seem to get both to happen simultaneosly. Ideally, they want the message signed with a different cert from the SSL client cert but they said we could use the same cert for both the SSL client authentication and to sign the message.

I'm willing to do anything at this point to get this working including using a CustomBinding if necessary.

I can get the SSL part working using the following:

var myBinding = new BasicHttpBinding();
myBinding.Security.Mode = BasicHttpSecurityMode.Transport;
myBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
var url = "";
EndpointAddress ea = new EndpointAddress(url);
var client = new SoapClient(myBinding, ea);
var certLoader = new CertificateLoader("password");
client.ClientCredentials.ClientCertificate.Certificate = certLoader.Load(@"c:\somecert.pfx");
var resp = client.somemethod(ref profile, new RequestType { version = RequestTypeVersion.Item100 });


please publish a sample working soap (e.g. one the vendor provided). btw there are much more drastic moves than using a custom binding :)

Edit: to use both transport and message security you need custom binding. Google for "wcf binding converter" to do this automatically. In the custom binding instead of http element change it to https and use attribute requirevlientcertificate. Sorry for spelling I'm on mobile


var c = new CustomBinding();            
MessageSecurityVersion version = MessageSecurityVersion.WSSecurity10WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11BasicSecurityProfile10;
var sec = SecurityBindingElement.CreateCertificateOverTransportBindingElement(version);
c.Element.Add(new TextMessageEncodingBindingElement() { MessageVersion = MessageVersion.Soap11 };)
c.Elements.Add(new HttpsTransportBindingElement() { RequireClientCertificate = true });


This is ugly but gets the job done. If you look at the elements of each of these custom bindings in the debugger, you'll see that even though both have an HttpsTransportBindingElement at the bottom of their stack, the TransportWithMessageCredential instance will have RequireClientCertificate set to false and a couple of other private memebers relating to the certificate set to false. The Transport instance will have these members set to true. This is clearly bug in WCF, IMO.

The code below takes the "good" HttpsTransportBindingElement from the customTransportSecurityBinding and replaces the "bad" one in the customMessageCredentialBinding. You then pass the modified customMessageCredentialBinding into your client constructor.

    var transportSecurityBinding = new BasicHttpBinding();
    transportSecurityBinding.Security.Mode = BasicHttpSecurityMode.Transport;
    transportSecurityBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
    transportSecurityBinding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.Certificate;
    var customTransportSecurityBinding = new CustomBinding(transportSecurityBinding);

    var messageCredentialBinding = new BasicHttpBinding();
    messageCredentialBinding.Security.Mode = BasicHttpSecurityMode.TransportWithMessageCredential;
    messageCredentialBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
    messageCredentialBinding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.Certificate;
    var customMessageCredentialBinding = new CustomBinding(messageCredentialBinding);

    // replace transport binding element from message credential binding with the transportSecurityBinding transport binding
    // which will include the cert for SSL mutual auth.
    customTransportSecurityBinding.Elements[customTransportSecurityBinding.Elements.Count - 1] = customMessageCredentialBinding.Elements[customMessageCredentialBinding.Elements.Count - 1];

var client = new SomeSoapClient(customTransportSecurityBinding, ea);


In case if anyone looking for app.config version of custom binding for the above answer:

        <binding name="SignedMessageBinding">
          <security messageSecurityVersion="WSSecurity10WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11BasicSecurityProfile10" /> 
          <textMessageEncoding messageVersion="Soap11" />
          <httpsTransport requireClientCertificate="true"  />