Monday, March 03, 2008

WCF: WS-Security, SSL, TransportWithMessageCredentials, Message Logging

I'm currently on a project where we're using WCF to interop with services provided by Spring-WS hosted on WebSphere. The basic WS-Security UsernameToken credential type over a basicHttpBinding with SSL was chosen for authentication.

The first problem we got was that the self-signed SSL certificate was not accepted by SvcUtil.exe or "Add service reference":

The remote certificate is invalid according to the validation procedure. Could not establish trust relationship for the SSL/TLS secure channel. The remote certificate is invalid according to the validation procedure.

Sure enough, opening the URL in MSIE showed two certificate errors: issuer trust and certificate common name/site name mismatch. The first issue was easily solved, the latter was not. However, a workaround is to save the WSDL to a file and use that file as the input to SvcUtil.exe. Of course, the same invalid SSL certificate will come back and bite you when trying to use the proxy - more on this later.

I took the generated the proxy code and config output and merged it into my code. Unsurprisingly, the unit test failed with the message "no WS-Security headers found in request". I turned on message logging on both service and transport level using SvcConfigEditor.exe, remembering to set LogEntireMessage to true. Sure enough, no wsse:UsernameToken element in the soap:Header. Grief...

A lot of research lead me to the knowledge that WCF will by design not allow any sensitive data to be sent on an unsecure channel. Time to look at the generated output.config. After a few hours of frustrated config tuning and log inspections, the issue was solved: SvcUtil.exe generates config that combines Username credentials with Transport mode security. This looks OK and gives no config validation exception, but it is absolutely not correct. This is the correct config for using WS-Security over basicHttpBinding and SSL:

<security mode="TransportWithMessageCredential">
<transport clientCredentialType="None" proxyCredentialType="None"realm="" />
<message clientCredentialType="UserName" algorithmSuite="Default" />

WCF will not put any wsse: headers into the request unless TransportWithMessageCredential is used as the security mode. In addition, add the username and password using the .Username and .Password properties of the proxy.ClientCredentials.UserName object.

After a while it occurred to me that the Spring-WS generated WSDL contained no WS-Policy statements, thus there was no guidance in the WSDL for creating the security artifacts for the proxy and config.
Now my request went through to WebSphere! But, how long was I in paradise? Until the response came back:

System.ServiceModel.Security.MessageSecurityException: Security processor was unable to find a security header in the message. This might be because the message is an unsecured fault or because there is a binding mismatch between the communicating parties. This can occur if the service is configured for security and the client is not using security.

The problem is that WCF expects a timestamp in the response when it puts a wsse: timestamp in the request (see the transport level message log). I have not been able to turn the timestamp off using config for <basicHttpBinding>, but it can be done using a <customBinding> (see Kristian Kristensen's blog). Nicholas Allen provides code and a deeper explanation of the timestamp mechanism in his blog:

BindingElementCollection elements = proxy.Endpoint.Binding.CreateBindingElements();
elements.Find<SecurityBindingElement>().IncludeTimestamp = false;
proxy.Endpoint.Binding = new CustomBinding(elements);

While you wait for the WebSphere folks to install a valid certificate, you can intercept the certificate validation and override any SSL policy violations:

using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;...
ServicePointManager.ServerCertificateValidationCallback = 
delegate(object sender, X509Certificate certificate, X509Chain chain, 
         SslPolicyErrors sslPolicyErrors)
//ignore invalid SSL: allow this client to communicate with unauthenticated serversreturn true;

At last my WCF client is able to invoke the web-service providing the expected WS-Security wsse:UsernameToken. What a wonderful interop world it is using WS-*.

Articles by IBM: Achieving Web services interoperability between the WebSphere Web Services Feature Pack and Windows Communication Foundation Part 1 and Part 2.


rwiemer said...

The solutions in this article were helpful to me in getting a .Net 3.5 WCF client to call an Oracle 10g app server behind an Oracle web services manager security system. In addition to the security mode setting of TransportWithMessageCredential and the Timestamp adjustment I had to specify SSLv3 to override the default TLS using:
System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Ssl3;

Anonymous said...

I don't have experience with WCF and your article proved invaluable to allow me to connect a .NET 3.5 app with a JBoss installed service.

Tks a million!!!

Anonymous said...

Thanks for leaving this post out here. I am a couple years behind you, but I encountered the very same problem. I am using a .Net, WCF client to consume a service hosted on JBoss that is secured using WS-Security. And the wsHttpBinding I expected to use in the client config file would not work. So this article gave me a solution when I worried there was none. Thanks for this!

Anonymous said...

You've put together some very valuable info.
Regarding SvcUtil.exe and the host name mismatch with SSL certificate I often put following in SvcUtil's config file:

In my current work we have IIS cluster with 2 nodes and this helps me a lot when I want to use WcfTestClient because by default mex binding is published via node's hostname rather than the whole cluster name.
You can configure IIS to solve this issue, but that's another thing.
It is required when you can publish your WSDL only over HTTPS.
I can provide more info about that via e-mail:

Anonymous said...

I had the same problem (part of it) when trying to use

no information have been passed.
The solution was to change mode to "TransportWithMessageCredentials"

Anonymous said...

Hi.. I have been searching for this solution for past one week.. Thanks dude