3
votes

This seems to be a common error (there are other posts with similar issues) - however, I have gone through all those posts and MSDN articles ( https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/working-with-certificates ). Scenario: Trying to access a service with an HTTPS end point. Setting the client certificate in code (certificate is loading correctly). As for the Server cert, I have tried both the options below: client.ClientCredentials.ServiceCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None;

client.ClientCredentials.ServiceCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.PeerOrChainTrust;

I have imported the server certificate to Personal as well as machine store (Trusted Root certificate authorities / certificates). The weird thing is the call is going through when I use Charles Proxy as the SSL proxy. Other settings:

    System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Ssl3;

    ServicePointManager.ServerCertificateValidationCallback +=
            (se, cert, chain, sslerror) =>
            {
                //Console.WriteLine(cert.GetCertHashString());

                if (cert.GetCertHashString() == "[actual hash here]")
                    return true;
                else
                    return false;
            };

The above Hash check works fine when Charles proxy is running. Without the proxy running, the callback does not even get called.

Any feedback is appreciated.

(It may be worthwhile to note that a Java client using Apache CXF library works fine - against the same service.)

Update: For completeness, the original error also had this text: This could be due to the fact that the server certificate is not configured properly with HTTP.SYS in the HTTPS case. This could also be caused by a mismatch of the security binding between the client and the server.

1

1 Answers

6
votes

OK, after days(& nights) of head banging, the following are my musings / findings (& of course the solution !):

  • There is "SSL" and then there is SSLv2, SSLv3, TLSv1.0, TLSv1.1, TLS1.2 & TLSv1.3 (draft as of now).

  • It is critical that the server and client are able to negotiate & pick one of these versions to successfully communicate.

  • The HTTP.SYS error seems to be a result of the client not being able to negotiate with the server on the appropriate version. When going through Charles proxy, it was clear that both Charles and the service we were trying to hit, were using TLSV1.1.

  • In my case, I was using wsHTTPBinding & though I tried setting the System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls; and other combinations, I could never get the HTTP.SYS error to go away. It would seem that the server and the client could never pick a version that they could agree on.

  • I did try using other bindings such as basicHttpBinding (with TransportWithMessageCredential) as well as basicHttpsBinding, but to no avail. What's more with some minor tweaks in the binding elements (through config & code) in each case, I ended with exactly the same binding configuration in all 3 cases (basicHttp/basichHttps/wsHttp bindings)! In essence, while there are these out-of-the-box bindings, they probably work for the most simple of scenarios. What's more, there is probably no need for so many of these pre-packaged bindings, especially as they seem to be using mostly the same binding elements.

  • I did remember reading that using a custom binding is better in many cases - but I imagined that by customizing a wsHttpBinding I would be achieving the same thing. Looks not - as there are some hard-coded properties (e.g.: default SSL protocols) in this binding that seem difficult to get around. I did take a look at the source code of wsHttpBinding and its base class, but could not find the exact hard coded location (but there are references to "default" protocols in the System.ServiceModel code).

    • In the end a "CustomBinding" worked for me, configured like so:

Custom Binding configuration - Sorry for including this as an image - as the formatting on SO was playing up.

  • The idea is to use httpsTransport with requireClientCertificate, security with authenticationMode="CertificateOverTransport" & includeTimestamp="true" (our service required Timestamp) and the relevant messageSecurityVersion - in our case it was: WSSecurity10WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11BasicSecurityProfile10.

  • The above configurations automatically signed the Timestamp as well.

  • On top of this we had to include the username / password credentials. Simply setting the client.ClientCredentials.UserName.UserName & client.ClientCredentials.UserName.Password did not result in these credentials included in the Security header. The logic was to add the username "token" as well, like so:

    //Get the current binding 
    System.ServiceModel.Channels.Binding binding = client.Endpoint.Binding;
    
    //Get the binding elements 
    BindingElementCollection elements = binding.CreateBindingElements();
    
    //Locate the Security binding element
    SecurityBindingElement security = elements.Find<SecurityBindingElement>();
    
    //This should not be null - as we are using Certificate authentication anyway
    if (security != null)
    {
        UserNameSecurityTokenParameters uTokenParams = new UserNameSecurityTokenParameters();
            uTokenParams.InclusionMode = SecurityTokenInclusionMode.AlwaysToRecipient;
        security.EndpointSupportingTokenParameters.SignedEncrypted.Add(uTokenParams);
    }
    
    client.Endpoint.Binding = new CustomBinding(elements.ToArray());
    
  • With all this setup, I was able to finally hit the Service and actually get the result - well, almost ! - as the result does not include a Timestamp, which WCF is throwing up as an exception. That is another problem to solve though.

Hopefully readers find this useful.

Update:

  • Now the Timestamp issue is also "sorted". The thing is the response lacked any security header, not just the timestamp. Thankfully there was a straightforward way to notify WCF to ignore unsecure responses, by simply marking an attribute on the security element: enableUnsecuredResponse="true". Obviously this is not desirable, but as we do not have any control on the service, this is the best we can do at the moment.