views:

828

answers:

2

I'm using wsHttpBinding with TransportWithMessageCredential, message clientCredentialType="UserName"

Trying to configure my service client to work against my public deployed address, I tested first by changing to "localhost", since localhost is the same IIS instance, just going through loopback instead of my PC's hostname. With loopback it fails validation.

My client config looks like:

<client>
  <endpoint address="https://win7/Services/Foo.svc"
    behaviorConfiguration="ServiceCertificate"
    binding="wsHttpBinding"
    bindingConfiguration="WSHttpBinding_IFoo"
    contract="FooReference.IFoo"
    name="WSHttpBinding_IFoo">
    <identity>
      <dns value="www.foo.net" />
    </identity>
  </endpoint>
</client>
 <behaviors>
    <endpointBehaviors>
       <behavior name="ServiceCertificate">
          <clientCredentials>
             <serviceCertificate>
                <authentication certificateValidationMode="PeerTrust" revocationMode="NoCheck"/>
             </serviceCertificate>
          </clientCredentials>
       </behavior>
    </endpointBehaviors>        
 </behaviors>

WIN7 is the name of my workstation. My service call works fine with WIN7. However, if I change the address to https://localhost/Services/Foo.svc it fails with a CommunicationObjectFaultedException.

According to Juval Lowy's Programming WCF Services book, on page 497:

WCF by default expects the service certificate name to match the service host's domain (or machine name). To compensate, the client must explicitly specify the test certificate name in the endpoint identity's DNS section:

He indicates the certificate must be in the "Trusted People" store (which it is). Then he gives the sample, which is what I used to base my client config above.

What I've found, though, is in my situation, no matter what I change dns value to, the client validates fine (seem to ignore this part). It is only when I change the address of the endpoint that the failure happens, and when I change it, I would expect the client to lookup the certificate by the value in the dns attribute. It seems not.

I also refer to MSDN reference: http://msdn.microsoft.com/en-us/library/ms733130.aspx under "Identify Checking at Runtime"

DNS. WCF ensures that the certificate provided during the SSL handshake contains a DNS or CommonName (CN) attribute equal to the value specified in the DNS identity on the client.

I have indeed loaded this certificate in the "Trusted People" store (as well as the Personal (My) store just for convenience all around).

Essentially, I have to be able to change the address to production, but I cannot even get the address change working with localhost, much less my public site.

A: 

Loading into the trusted people store stops CRL lookups or errors if there is no CA backing the cert, but it does not stop the checking of host names - which is a good thing. The host name matching shows that the certificate is on the correct machine.

As self signed certs are typically issued to the machine name, not localhost then what you're attempting is not going to work. Of course if you put a certificate on the product box with the correct common name and from a trusted CA all will work as expected.

blowdart
I don't quite follow you. I'm using X509CertificateValidationMode.PeerTrust, which means look in Trusted People.Also, even so, I've loaded my own CA cert into both prod box and local box, under Trusted Certificate Authorities, so there should be a chain there.It was issued to www.foo.net, which does not match either localhost or WIN7.
mrjoltcola
I'm also specifying the cert explicitly on the host side in my serviceCertificate.
mrjoltcola
Yes but if the host name of the address you are listening on and the subject of the certificate you are loading differ then none of that has any effect.
blowdart
That is the point of the quote from Lowy's book in my question above. Using PeerTrust, we are supposed to be able to override the behaviour with an identity dns value. If the hostname doesn't match, it is supposed to point to the name of my test certificate. The fact that I'm using a domain name above might be confusing, assume it is FOO. If what you say is true, his book is wrong. But I found out through testing that even with PeerTrust in my config, the cert is accepted even if it is NOT in my local store, because the CA cert IS in there. Apparently WCF is still using ChainTrust.
mrjoltcola
Well Lowy knows more about WCF that me, but I had always though the identity value was to allow you to set the FQDN it responds to on multihoned machines, or on IPs which have multiple FQDNs. However I'd have to go experiment to make sure. Chain trust is different, it checks for a CRL, but that doesn't affect the matching host name check.
blowdart
+1  A: 

I've recently run into a similar issue and figured I would lend my 2c here. WCF ignores clientCredentials/serviceCertificate/authentication configuration settings when using transport level security -- it only applies with message based security. This was definitely not obvious to me until I started debug tracing and watched my settings being ignored, and 'Custom' validators never called.

Another SO post here helped confirm this for me -- so thanks to Sharjeel Ahmed -- http://stackoverflow.com/questions/1559915/custom-certificate-validation-in-wcf-service who basically links to another blog post here -- http://blog.coditate.com/2009/04/confusing-errors-using-wcf-transport.html

That said, the way to provide custom cert validation (which is what you need based on the hostname for the cert not matching the host returning it) is to use a bit of a nasty hack -- the nasty part being that you provide a callback that is used globally! In my application, I do provide some configuration that helps me do this a little more safely by comparing thumprint and dns hostname -- but I would definitely keep in mind that this can be a dangerous proposition if you're not dealing with a controlled environment. (I'm going back-end server to server within an Intranet in my case).

Here's some sample code... at the point it's called, the certificate chain should have be fully created / validated (and hence you can see that policyErrors have been filled in) -- this callback provides the ultimate valid / invalid. In my case, I just try to match config and if it's not a match, I go by the already filled in policyErrors.

ServicePointManager.ServerCertificateValidationCallback += (o, cert, chain, policyErrors) =>
{                   
   string hostName = string.Empty;
   if (o is string) hostName = (string)o;
   else if (o is WebRequest) hostName = ((WebRequest)o).RequestUri.DnsSafeHost;
   string thumbprint = cert.GetCertHashString();

   if (Settings.Current.WebUtility().CertificateValidator.ValidThumbprints.Thumbprints
   //if we've configured this thumprint + host, then we're good
   .Any(config => String.Equals(Regex.Replace(config.Thumbprint, @"\s+", ""), thumbprint, StringComparison.CurrentCultureIgnoreCase)
   && String.Equals(config.Host, hostName, StringComparison.CurrentCultureIgnoreCase)))
   {
      log.InfoFormat("Considering certificate with thumbprint {0} and host {1} as valid based on configuration", thumbprint, hostName);
      return true;
   }

   return policyErrors == SslPolicyErrors.None;
};
Ethan J. Brown