Tuesday, February 23, 2010

Hosting Https SSL WCF as a Windows Service

There may have various reason where you opt to host a SSL WCF service as Windows Service compare to IIS. One of those reason is  existing TCP service for internal folks and now you want to expose it to a web. Well, ideal way is to open another https endpoint. Now a general consensus among developers is TCP should be hosted in Windows service and HTTP(S) in IIS. So now that you have two endpoints TCP and HTTPS, IIS 6.0 and below doesn't fit your need.
Plus, you have a service hosted on windows service, running under TCP for a long time, well tested and doing good its job. This is the ideal scenario where you need this path.

Alright, without wasting a single minute on it, let jump to the code. In order to accomplish this task, you need the following ingredients.
  1. A WCF that has been secured by Transport Layer Security mode.
  2. A Windows Service to host this service.
  3. Server, client and trust root certificates for trust chain.
  4. Httpcfg.exe utitly.
  5. A client, console application that uses client certificate to call this secure service.
If you don't have these ingredients, please read this tutorial from this point. Otherwise jump to the section "Configuring HTTPS WCF Service with Httpcfg.exeutility".

Creating aWCF Service
Create a new C# Class Library project. Name it as Service. Since you arecreating a new Project, a new solution will also be created. Select the location of thisproject/solution file and rename the solution WCF or something of your own choice.
Click OK.






Let’s quicklyrename the Class1.cs file to something like Calculator in Service project. Make sure the classname also changes to Calculator.








Don’t forget to add System.ServiceModel.dll as areference to Service project as shown below.




Now let’s addcouple of methods in it such as Add () and Subtract (). Pretty basic, huh?
Please copyand paste the following code in your Calculator.cs to save time.




using System;

usingSystem.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
namespace Service
{
   [ServiceContract]
   public classCalculator
   {
       [OperationContract]
       public intAdd(int x, inty)
       {
            returnx + y;
       }
       [OperationContract]
       public intSubtract(int x, inty)
       {
            returnx - y;
       }
   }
}



Note: You can create your own WCF by any mean, but for this tutorial please follow the one.


Okay. Now at this point we have created a simple WCFservice that has 2 methods for Addition and Subtraction.
Let’s now Create a Windows Service that will host thisWCF service.
Right click on your WCF solution file and select Add New Project then select Windows Service.






Alright now we have 2 projects in our solution. First oneis WCF service and 2nd one is Windows Service.
Hosting WCF serivce under Windows Service
Double Click on Service1.cs file in WindowsServiceproject go open the file in Design mode. Then right click on design mode andselect Add Installer as shown in the figure below.








This actionwill bring 2 components as shown in the following figure.
Note: Don’t forget to add areference of CalculatorService and System.ServiceModel in your WindowsService.






Ok now let’scome back to the code and rename Service1.cs to something meaningful; umm likeCalculatorService.cs. Now open up the code editor of CalculatorService.cs bydouble clicking and paste the following code in your CalculatorService class.


public partial class CalculatorService : ServiceBase
   {
       private ServiceHosthost;
       public CalculatorService()
       {
            InitializeComponent();
       }

       protected overridevoid OnStart(string[]args)
       {
            try
            {
                if(host != null)
                {
                    host.Close();
                }

                host = newServiceHost(typeof(Service.Calculator));

                host.Open();
            }
            catch(Exception ex)
            {
                throwex;
            }
       }

       protected overridevoid OnStop()
       {
            if(host != null)
            {
                host.Close();
                host = null;
            }
       }
   }



This codeis simply creating a service host for type calculator WCF service referenced inour project.
Before weinstall this windows services let’s do some tweaks to the installer files.
First ofdouble click on ProjectInsaller.cs file to open it in Design mode and thenclick on ServiceProcessInstaller1 component.
Press f4 tosee its properties if not shown by default. Change the Account property fromUser to LocalService.
Now Click onServiceInstaller1 and change the following properties.
ServiceNamefrom Service1 to CalculatorService or something meaningful.
StartTypefrom Manual to Automatic.
Display namefrom empty to Calculator Service or something meaningful.
Description:  A robust calculator or whatever you want.

Till now,we havecreated a service and its host but we really didn’t write ABC of our service.Most importantly we didn’t make our service SSL secure. So let’s do it. Butbefore we jump into it, we are missing some important things, "certificates"!
Creating and Configuring Certificates
Since this tutorialis about hosting a SSL service as windows service, we probably need server andclient certificates signed by a trusted certificate authority like VeriSign or Thwart.  To get certificates from these authoritiesfor development or learning purpose is not only costly but time consuming tooand is not recommended.
How to make client and servercertificate by using MakeCert .exe command.

If you wantto reinvent the wheel you can go to these link and create your own CA, Serverand client certificates. Otherwise you can use one of mine provided with thissolution to save your time.
If you areusing the certficiates which are provided with this solution, be sure to importclient and server certificates to Personal Folder. If you don’t know how to doit read below for step by step instructions.
How to import certificates to appropriate folders
  1. Log on to the system as an administrator.
  2. Click Start, click Run, type mmc, and then click OK.
  3. On the File menu, click Add/Remove Snap-in, and then click Add.
  4. Under Snap-in, double-click Certificates, click Computer account, and then click Next.
  5. To manage certificates for the local computer, click Local computer, and then click Finish. .
  6. Click Close.
Certificates(Computer Name) appears on thelist of selected snap-ins for the new console.
  1. If you have no more snap-ins to add to the console, click OK.
  2. To save this console, on the File menu, click Save.
  3. Click on(+) sign to expand the Certifcates tree.
  4. Right click on Personal Folder and follow the steps to import Server.cer and Client.cer Certificates.
  5. Right click on Trusted Root Certification Authorities to import Our Certificate Authority.cer

Note: Thepassword for the certificate’s private key for both client and server is 1234
Alright, nowwe have created and imported these 3 certificates. Be sure that client andserver certificates should have private key with them as shown in the followingfigure.











Right clickon WindowsService Project and click on Add à New Item. Let’s add App.config.





Alright, I amgoing to configure my service based on config file as compared to write code.But you are free to choose whichever way you want to.
Since theintent of this tutorial is to teach how to host a SSL WCF service as a windowsService and not to teach how to create a windows service, I am skipping the ABCof WCF. You are free to write your ABC or copy one from the below.




<xml version="1.0" encoding="utf-8" ?>
<configuration>
 <system.serviceModel>

   <behaviors>
      <serviceBehaviors>
        <behavior name="OurServiceBehavior">
          <serviceCredentials>
            <clientCertificate>
              <authentication certificateValidationMode="ChainTrust" revocationMode="NoCheck"
                trustedStoreLocation="LocalMachine" />
            clientCertificate>
            <serviceCertificate findValue="24d7ac65704bc0a161cc2539d22ad2916f5cf4b0"
              storeLocation="LocalMachine" storeName="My" x509FindType="FindByThumbprint" />
          serviceCredentials>
          <serviceMetadata httpsGetEnabled="true" />
        behavior>
      serviceBehaviors>
   behaviors>

   <services>
      <service behaviorConfiguration="OurServiceBehavior" name="Service.Calculator">
        <host>
          <baseAddresses>
            <add baseAddress="https://localhost:8022/CalculatorService" />
          baseAddresses>
        host>
        <endpoint address=""
          binding="wsHttpBinding" bindingConfiguration="SecureBinding"
          contract="Service.Calculator" />
        <endpoint address="mex" binding="mexHttpsBinding" contract="IMetadataExchange" />
      service>
   services>

   <bindings>
      <wsHttpBinding>
        <binding name="SecureBinding" closeTimeout="00:010:00"
          openTimeout="00:010:00" receiveTimeout="00:10:00" sendTimeout="00:010:00"
          bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard"
          maxBufferPoolSize="524288" maxReceivedMessageSize="65536" messageEncoding="Text"
          textEncoding="utf-8" useDefaultWebProxy="true" allowCookies="false">
          <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
            maxBytesPerRead="4096" maxNameTableCharCount="16384" />
          <reliableSession ordered="true" inactivityTimeout="00:10:00"
            enabled="false" />
          <security mode="Transport">
            <transport clientCredentialType="Certificate" proxyCredentialType="None"
              realm="" />
          security>
        binding>
      wsHttpBinding>
   bindings>

 system.serviceModel>
configuration>







Let’s walkthrough couple of snippets in this configuration.



Services Element:



This aboveconfig snippet will create a WCF service “Service.Calculator”whose address is http://localhost:8022/CalculatorService. It means that service only exposes a singleendpoint on SSL available at https://localhost:8022/CalculatorService. Although you can expose anotherendpoint which is unsecure for internal connection like TCP. The services uses wsHttpBinding and which uses SecureBinding as a configuration (willbe discussed below).
The servicealso exposes another endpoint called mexat the address which exposes metadata i.e. WSDL.
There isanother property called contract which is in this tutorial is a class and notan interface. Please remember that this is not a WCF design session ortutorial, that’s why I have skipped many of industry standards orrecommendations here.


Bindings Element:



You guysprobably are familiar with bindings so I am not going to discuss it in depthexcept the security element of this.
This is the elementthat defines whether the web service is secure or not, if it is secured thenwhich mode it is; Transport layer security (TLS/SSL) or Message Based Security(MBS). We are using TLS with Certificates and no other, username, windows etc.
Until now wehaven’t actually made our service secure. We need to provide a servicecertificate. Which server will present to client to prove its identity at thetime of security handshake. Let’s jump on that section.


Behaviors Element:



Okay, so thissection defines the behavior of the service. How should the service behave?
First let’sdiscuss the following elements inside behavior tag.



ServiceCertificate Element:



This tells WCF that go find the certificate from My store of LocalMachine location. The finding method is finding it bycertificate thumbprint whose value is 24d7ac65704bc0a161cc2539d22ad2916f5cf4b0.

This sentence will be translated by windows as go find thecertificate in Personal Folder of Computer Account location whosethumbprint value matches 24d7ac65704bc0a161cc2539d22ad2916f5cf4b0

How to get certificate thumbprint:
Now for thosepeople who don’t know how to get thumbprint I can show you by taking extrapain.
Double click on your Our Servercertificate in the personalà Certificates Folder. Click on 2ndtab Details and scroll all the way down, youwill see thumbprint as shown the figure below.







I could have search the certficate by other means, subject name,expiration date, issuer etc but I prefer thumbprint as this is unique for eachcertficiate.
Note: Did you see that I have pasted thumprint without spaces? Veryimportant.
ClientCertificateElement:



Client Certificate Element:



Now what doesthis script telling us? This is merely saying 3 things. When client tries toconnect service, it will present WCF service a certificate. WCF service has to trustthe certificate chain which is present in its LocalMachine location in windows certificate store. Also you don’t need to checkonline whether the certificate has been revoked or not at this stage.
If you haveimplemented PeerTrust instead of ChainTrust, the scenario will bedifferent. In that case you must have installed client’s public key certificatealong with its chain of issuers in your server certificate stores inappropriate folder. Similarly, the client have to install server public keycertificate along with its trust chain in client’s computer‘s certificatestores in appropriate folders. Also in this case, the config file will slightlychange and you have write configuration tofind client certificate. Same code have to be written in client’s config tofind the server public cert. 

Installing the Service
Installation using installutil.exe:
Let’s installour service now and see if it is working or not. You can create a setup projectto install or use the following method which I am using to install thisservice. This is quick way to install and debug the issues as compared to setupproject.
Installutil.exeis use to install and uninstall windows service that serves an efficient toolin development.
Go to theVisual Stuido 2010 command prompt and type the following command to install theWindowsService.






Oppose to theabove command you can use installutil /u [exe path] to uninstall the service.





Alright,after installing the service go to the Services component under Control Panel--> Administrative tools-->Services or you can type services.msccommand in the run dialog box to open this quickly.





Starting the service:
See ourcalculator service is installed and ready for running. Click on Start to runthe service.
Go tointernet explorer and type the following address in the browser.
Hmmm... Whywe aren’t getting the WSDL? Yes we have enable https metadata in server config.Also we have provided the correct certificate chains, thumbprint, behavior anddouble check everything else. What is the problem now?
Configuring HTTPS WCF Service with Httpcfg.exeutility
This is heartof this tutorial. We forgot to configure the port (8022) with a X.509certificate. Why? Here is an explanation from MSFT.
Ok, now thatyou have understood the necessity of this command line tool. Let’s start tobind our certificate to the port.
For windowsXP, For Vista and Windows server 2008 you would have get netsh.exe which is outof scope of this tutorial.
Note: Ifin future the download location of httpcfg.exe changes, you can always get itfrom the solution zip file I am providing you.
Okay, nowlet’s open up windows command prompt and go to the directory where httpcfg.exeexists. In this case it is in c:\blog\httpcfg.exe.
Type thefollowing command to see how many ports are bound to X509 certificate and theone you are using is not in there.


 
C:\Blog> httpcfg query ssl



In mycomputer only 1 port 8021 is bound to some X509 certificate whose hash (Thumbprint)is not the one which our certificate has. So no problem
Let’s configure 8022 port to the server certificate whosethumbprint is 24d7ac65704bc0a161cc2539d22ad2916f5cf4b0

The commandfor binding the port to Cert is


 
C:\Blog\WCF> httpcfg set ssl -i0.0.0.0:8022 –h 24d7ac65704bc0a161cc2539d22ad2916f5cf4b0–f 2


If you receivethe following message it means you are successful.
HttpSetServiceConfiguration completedwith 0.

Let’s queryagain to see how many ports are bound to which certs and if 8022 is one of themor not.





Do you seethat? We have configured port 8022 with the Server certificate whose thumbprintis same we are using in our server app.config.
Good deal.Try browsing the URL again.



Click on ViewCertificate button. Yes it is the server certificate that we have provided toWCF.









Click on Okbutton followed by Yes button tocontinue. You will probably see this page, but don’t worry about it as ourserver certificate is not issued to this machine name. Click on continue to this website (not recommended) link.








Aha! Here wesee our secured https WCF service hosted as windows services exposing itsmetadata in a WSDL.





Here is theWSDL screen shot.





Good deal,isn’t it?
Creating WCF Client:
Ok now thatservice is running we want our clients to connect to it. Let’s make a simpleconsole application client which will call this service to achieve the goal.
Right clickon solution file-->Add-->New Project and select Console Application. Rename the projectas Client and press ok.
Next step isto add a service reference. Right click on Client project and click on Add Service Reference.
Type thefollowing address to add a service reference and click on Go button.
Note: Ifyou receive an error warning from Visual Studio that says “Visual Studio hasdetected a problem with site’s security certificate”, please ignore it by clickyes.

Click okbutton add this as a service reference.
Note: Did yousee Visual Studio automatically creates app.config file with a bunch of configelements in it? This is the magic of VS that is only available to .Net developers.





Double clickon Program.cs file in Client Project to write the following code snippet.

CalculatorService.CalculatorClient proxy = newCalculatorService.CalculatorClient();
Console.WriteLine("The Result of Addition is:" +proxy.Add(2, 2));
Console.ReadLine();

Okay folks itis time to hit F5 (if client is your startup project).
Did you getthe following picture too? If not then you are doing something wrong.






Well ofcourse it was expected, how does VS know which certificate you want to provide?
Yes, weforgot to write configuration to provide client certificate. Remember wealready created and imported a client certificate into our personal folder?
Again, thereare 2 ways to achieve this goal, via coding and via configuration. I opt in forconfiguration.
Let’s add thefollowing configuration snippet in your app.config in Client Project.


 
<behaviors>
        <endpointBehaviors >
          <behavior name="SecureEpBehavior">

            <clientCredentials>
              <serviceCertificate>
                <authentication certificateValidationMode="ChainTrust"/>
              serviceCertificate>
<clientCertificate
findValue="5491c10dcf012c91008a6b2857b3bd2c13ab9791"
storeLocation="LocalMachine" storeName ="My" x509FindType="FindByThumbprint" />
                        clientCredentials>

          behavior>
        endpointBehaviors>
behaviors>



Don’t forgetto add this behavior name “SecureEpBehavior” in behaviorConfiguration elementin tag as highlighted below.
 



<client>
            <endpoint address="https://localhost:8022/CalculatorService/CS"
                binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_Calculator" behaviorConfiguration="SecureEpBehavior"
                contract="CalculatorService.Calculator" name="WSHttpBinding_Calculator" />
        client>


The fullconfig file will be shown as below.


 
xml version="1.0" encoding="utf-8" ?>
<configuration>

  
    <system.serviceModel>
      <behaviors>
        <endpointBehaviors >
          <behavior name="SecureEpBehavior">

            <clientCredentials>
              <serviceCertificate>
                <authentication certificateValidationMode="ChainTrust" trustedStoreLocation="LocalMachine" revocationMode="NoCheck"/>
              serviceCertificate>
              <clientCertificate findValue="5491c10dcf012c91008a6b2857b3bd2c13ab9791"
                storeLocation="LocalMachine" storeName ="My" x509FindType="FindByThumbprint" />
            clientCredentials>

          behavior>
        endpointBehaviors>

      behaviors>
     
        <bindings>
            <wsHttpBinding>
                <binding name="WSHttpBinding_Calculator" closeTimeout="00:01:00"
                    openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
                    bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard"
                    maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
                    messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true"
                    allowCookies="false">
                    <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
                        maxBytesPerRead="4096" maxNameTableCharCount="16384" />
                    <reliableSession ordered="true" inactivityTimeout="00:10:00"
                        enabled="false" />
                    <security mode="Transport">
                        <transport clientCredentialType="Certificate" proxyCredentialType="None"
                            realm="" />
                        <message clientCredentialType="Windows" negotiateServiceCredential="true" />
                    security>
                binding>
            wsHttpBinding>
        bindings>
        <client>
            <endpoint address="https://localhost:8022/CalculatorService"
                binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_Calculator" behaviorConfiguration="SecureEpBehavior"
                contract="CalculatorService.Calculator" name="WSHttpBinding_Calculator" />
        client>
    system.serviceModel>
configuration>




Let’s try onemore time by press F5. This time we should see a message that we have neverseen as shown below.





Now what? Ifyou click on View Details… under Actions: Go to InnerException and go to InnerException of this InnerExcepiton.You will see a message that says “Theremote certificate is invalid according to the validation procedures” asshown in the following picture.






You probablywill be wondering something is wrong with our server certificate or chain trustor method of validation and the doubt goes on and on until you get frustratedwhy on earth this message is coming. Well there is nothing wrong with ourcertificate except a minor problem with Certificate name that was not issued tothis machine or you can say that doesn’t resemble to this machine name.
And thatmakes completely sense, since you are in development mode and the server orclient certificate that you got is for production computer, if otherwise youget certificate of each environment. So how do we resolve this? Well there is aconfiguration that you need to put on top of your client configuration file sothat WCF doesn’t check for the certificate name anymore. Here is thatconfiguration snippet.


 
   <system.net>
      <settings>
       <servicePointManager checkCertificateName="false"/>
     settings>
    system.net>

So ourcomplete updated client app.config will look like below.


  
xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.net>
    <settings>
      <servicePointManager checkCertificateName="false"/>
    settings>
  system.net>

  <system.serviceModel>
    <behaviors>
      <endpointBehaviors >
        <behavior name="SecureEpBehavior">

          <clientCredentials>
            <serviceCertificate>
              <authentication certificateValidationMode="ChainTrust" trustedStoreLocation="LocalMachine" revocationMode="NoCheck"/>
            serviceCertificate>
            <clientCertificate findValue="5491c10dcf012c91008a6b2857b3bd2c13ab9791"
              storeLocation="LocalMachine" storeName ="My" x509FindType="FindByThumbprint" />
          clientCredentials>

        behavior>
      endpointBehaviors>

    behaviors>

    <bindings>
      <wsHttpBinding>
        <binding name="WSHttpBinding_Calculator" closeTimeout="00:01:00"
            openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
            bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard"
            maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
            messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true"
            allowCookies="false">
          <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
              maxBytesPerRead="4096" maxNameTableCharCount="16384" />
          <reliableSession ordered="true" inactivityTimeout="00:10:00"
              enabled="false" />
          <security mode="Transport">
            <transport clientCredentialType="Certificate" proxyCredentialType="None"
                realm="" />
            <message clientCredentialType="Windows" negotiateServiceCredential="true" />
          security>
        binding>
      wsHttpBinding>
    bindings>
    <client>
      <endpoint address="https://localhost:8022/CalculatorService"
          binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_Calculator" behaviorConfiguration="SecureEpBehavior"
          contract="CalculatorService.Calculator" name="WSHttpBinding_Calculator" />
    client>
  system.serviceModel>
configuration>


Try pressingF5 now. I am positive you will the screen below if you have followed thetutorial thoroughly and didn’t miss or skip any step.






Happy ending :)
Source Code

9 comments:

  1. Great work Talal. This is really useful for 2 way TLS.

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. Excellent article! I did notice that with Windows 8, you'll have to use netsh instead of httpcfg. Regardless, this saved me a ton of time.

    ReplyDelete
  5. Hi Talal! Great article. I followed it all the way to the end but I could not solve last error ("Could not establish trust relationship...").

    I believe reason for that was my inability to import certificates you provided so that private key is set. I found an article which suggests that after the import, cert store should be repaired, but with Win 10 I could not do it as command would ask for some Card reader to be connected!? So, I tried to create my own certificates using Powershell Cmdlet New-SelfSignedCertificate, but obviously I missed something.

    Can you please point me to an article which describes how to import certificates (*.cer files) so that private key stays set after importing?

    Thanks,
    Miljan

    ReplyDelete
  6. Does the Client use private key for communication? or is it just one way ssl where only server uses private key?

    ReplyDelete
  7. Free games online… [...]When I initially commented I clicked the “Notify me when new comments are added” checkbox and now each time a comment is added I get four emails with the same comment. Is there any way you can remove me from that service? Thanks![...]… https://hostinglelo.in/

    ReplyDelete