Certificate security for WCF over MSMQ communication

Step-by step guide for setting up certificate security for WCF over MSMQ communication

Author: Sergey Sorokin
Last updated: September 10, 2009
Disclaimer: this article is neither related to CSWorks web HMI framework nor should be associated with CSWorks Inc.

1. The plan

The goal is to implement a secured WCF communication based on MSMQ under the following requirements/assumptions/recommendations.

  1. All MSMQ traffic must be encrypted and signed.
  2. No involvement of Windows Domain security and Active Directory.
  3. No code changes required on the client or on the server side.
  4. MSMQ version 4.0 (W2K8) is used, but it would be nice to have a solution that works on MSMQ 3.0 (W2K3) as well.
  5. There must be a well-established and straightforward routine for setting up secured communication that can be followed during test/staging/production deployment.

The weapon of choice is message-based certificate security. To put it simple, it means two things:

This document describes the routine (5) in detail.

2. Tools

This section contains a brief overview of the toolset that one will need when setting up WCF security in our scenario. If you are following the routine described in this document, you may want to download all of these tools, you may find some of them in the "Attachments" section of this document.

2.1 cert.cmd

Windows shell script that uses some of the utilities listed below and facilitates certificate management.

2.2 Certificate files

Tool directory may also contain a number of certificate files (*.pfx or *.cer) that were generated by cert.cmd. Furthermore, we assume that we are dealing with the following certificate files:

2.3 makecert.exe

Certificate creation tool that generates X.509 certificates for testing purposes. This tool is a part of the .NET framework SDK, more information at http://msdn.microsoft.com/en-us/library/bfsktky3(VS.80).aspx.

2.4 pvk2pfx.exe

Just a convenience tool that copies public key and private key information contained in .cer, and .pvk files to a .pfx file. This tool is a part of the .NET framework SDK, more information at http://msdn.microsoft.com/en-us/library/dd434714.aspx.

2.5 certmgr.exe

Certificate management tool that facilitates the data exchange between certificate files and certificate stores. This tool is a part of the .NET framework SDK, more information at http://msdn.microsoft.com/en-us/library/e78byta0(VS.80).aspx.

2.6 findprivatekey.exe

Used to find the location and name of the private key file associated with a specific X.509 certificate in the certificate store. This tool is comes with the Sample Tools for .NET framework, more information at http://msdn.microsoft.com/en-us/library/ms732026.aspx.

2.7 capicom.dll

Microsoft cryptography API ActiveX . We use it for importing private keys to the certificate store. More information at http://www.microsoft.com/downloads/details.aspx?FamilyID=860ee43a-a843-462f-abb5-ff88ea5896f6&DisplayLang=en

2.8 cstore.vbs

A script that calls capicom.dll and performs some operations on certificates. Distributed as part of CAPICOM (see capicom.dll). Before running this script, make sure you have registered capicom.dll using regsvr32.exe (make sure you run the proper command on 64-bit systems).

2.9 httpcfg.exe

HTTP configuration utility. We used for configuring URL access to queue service on W2K3. More information at http://technet.microsoft.com/en-us/library/cc787508(WS.10).aspx.

3. Development

3.1 Simple Test Framework

Let's create a simple WCF service test framework

  1. Create 2 projects: client and WCF Server Library
  2. Implement a simple WCF method PostSomeData. Make sure you marked this method as one-way ([OperationContract(IsOneWay = true)]), otherwise you will get "System.InvalidOperationException: Contract requires TwoWay (either request-reply or duplex), but binding '...' doesn't support it or isn't configured properly to support it." error message.
  3. Run SimpleServiceLibrary - WcfSvcHost.exe ( http://msdn.microsoft.com/en-us/library/bb552363.aspx ) will host it.
  4. Add a reference to this service to the ClientApp and call it SimpleServiceReference.
  5. Implement a simple client that posts random integer values periodically.
  6. Run the client and make sure that the service processes the posted data (you may use dbgview.exe from SysInternals). If you have any problems with URL reservation, see Appendix A.

3.2 Make it use MSMQ

Let's use msmq instead of http as a transport (using MSMQ 3.0 on W2K3). No security for now.

3.2.1 Queue

Create a new private transactional queue SimpleQueue (make sure that "Everyone" has full access to it). The queue we are using has the following characteristics.

  1. MSMQ does not have Active Directory integration feature installed.
  2. Queue is private.
  3. Anonymous users can send, receive and peek messages.
  4. Queue is transactional

3.2.2 Client configuration

Configure client's system.ServiceModel section:

<client>
  <endpoint address="net.msmq://localhost/private/SimpleQueue" behaviorConfiguration="NoSecurityBehavior"
    binding="netMsmqBinding" bindingConfiguration="SimpleMsmqBinding" contract="SimpleServiceReference.ISimpleService"
    name="SimpleServiceEndpoint">
    <identity>
      <dns value="localhost" />
    </identity>
  </endpoint>
</client>
<behaviors>
  <endpointBehaviors>
    <behavior name="NoSecurityBehavior"/>
  </endpointBehaviors>
</behaviors>
<bindings>
  <netMsmqBinding>
    <binding name="SimpleMsmqBinding" closeTimeout="00:01:00" openTimeout="00:01:00"
      receiveTimeout="00:10:00" sendTimeout="00:01:00" deadLetterQueue="System"
      durable="true" exactlyOnce="true" maxReceivedMessageSize="65536"
      maxRetryCycles="2" receiveErrorHandling="Fault" receiveRetryCount="5"
      retryCycleDelay="00:30:00" timeToLive="1.00:00:00" useSourceJournal="false"
      useMsmqTracing="false" queueTransferProtocol="Native" maxBufferPoolSize="524288"
      useActiveDirectory="false">
      <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
        maxBytesPerRead="4096" maxNameTableCharCount="16384" />
      <security mode="None" />
    </binding>
    <binding name="SimpleService" closeTimeout="00:01:00" openTimeout="00:01:00"
      receiveTimeout="00:10:00" sendTimeout="00:01:00" deadLetterQueue="System"
      durable="true" exactlyOnce="true" maxReceivedMessageSize="65536"
      maxRetryCycles="2" receiveErrorHandling="Fault" receiveRetryCount="5"
      retryCycleDelay="00:30:00" timeToLive="1.00:00:00" useSourceJournal="false"
      useMsmqTracing="false" queueTransferProtocol="Native" maxBufferPoolSize="524288"
      useActiveDirectory="false">
      <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
        maxBytesPerRead="4096" maxNameTableCharCount="16384" />
      <security mode="None"/>
    </binding>
  </netMsmqBinding>
</bindings>

3.2.3 Server Configuration

Configure servers's system.ServiceModel section:

<services>
  <service behaviorConfiguration="NoSecurityBehavior"  name="SimpleServiceLibrary.SimpleService">
    <endpoint address="mex" binding="mexHttpBinding" name="Mex" contract="IMetadataExchange" />
    <endpoint address="net.msmq://localhost/private/SimpleQueue" binding="netMsmqBinding"
      bindingConfiguration="SimpleMsmqBinding" name="SimpleService" contract="SimpleServiceLibrary.ISimpleService" />
    <host>
      <baseAddresses>
        <add baseAddress="http://localhost:8731/ServerApp/SimpleService/" />
      </baseAddresses>
    </host>
  </service>
</services>
<behaviors>
  <serviceBehaviors>
    <behavior name="NoSecurityBehavior">
      <serviceThrottling maxConcurrentCalls="1" maxConcurrentInstances="1" maxConcurrentSessions="1">
</serviceThrottling>
      <serviceMetadata httpGetEnabled="True"  />
      <serviceDebug includeExceptionDetailInFaults="True" />
    </behavior>
  </serviceBehaviors>
</behaviors>
<bindings>
  <netMsmqBinding>
    <binding name="SimpleMsmqBinding" exactlyOnce="true" receiveErrorHandling="Fault">
      <security mode="None">
        <message clientCredentialType="None" />
      </security>
    </binding>
  </netMsmqBinding>
</bindings>

3.2.4 Try It

Run service and the client app. Make sure that service processes all PostSomeData calls. Note the size of the message in the message queue, it should take only a couple of hundreds of bytes. Now it's time to make our communication secure.

3.3 Certificates

This section discusses the workflow for the certificate management on the client (HXAPI web service) and server (Queue Service) machines.

3.3.1 Creating certificates

Certificate installation procedure begins with actual certificate generation. This section describes test and production certificate generation separately.

3.3.1.1 Generating test certificates

For test deployments, you may use cert.cmd script mentioned above to generate self-signed certificates. The following command

cert.cmd create

creates two private/public certificate key pairs: SimpleQueueServer and SimpleQueueClient (see "Tools"-"Certificate files" section above for details). After creation, you can copy the toolset to getether with generated .pfx and cer files to all server and client machines and install correspondent certificates (see "Installing certificates" section below.

3.3.1.2 Using existing certificates in production environment

For production environment, you may want to use your existing X.509 certificates signed by some well-know CA. If you want to install them using cert.cmd utility, you need to make sure you have two files for every private/public key pair: one .cer file with public key and one .pfx file that contains both private and public keys.
If you do not have a .pfx file for you production key pair, you may use pvk2pfx .exe utility mentioned above to generate the .pfx file from existing .cer and .pvk files. If you have a .pfx file, but do not have a correspondent .cer file, use certmgr.exe or cscript.vbs to extract public key from the .pfx file to the .cer file.
Keep in mind that you will have to modify cert.cmd and all WCF configuration files so they reference RealQueueClient and RealQueueServer instead of SimpleQueueClient and SimpleQueueServer. When done, you can proceed with installing certificates as described below.

3.3.2 Installing certificates

After you have copied .pfx and .cer files together with the toolset on a specific client or server machine, you can install correspondent keys to this machine's certificate store.

3.3.2.1 Client

On the client machine, assuming you are running your instance of client application under Network Service account, run the following command:

cert.cmd setupclient NetworkService

It will do the following:

3.3.2.2 Server

On the server machine, assuming you are running your SimpleService under Network Service account, run the following command:

cert.cmd setupserver NetworkService

It will do the following:

3.3.2.3 Single box scenario

If you are a developer/tester working with test deployment, you may want to run both client and server on your development machine. In this case, you have to install both private and public keys for both client and server on this machine, and give correspondent users access to those private keys. Assuming both client app and WcfSvcHost.exe are running under local "Administrator" account:

cert.cmd setupclientandserver Administrator Administrator

it will do the job for you.

3.4 Use Certificates with MSMQ

3.4.1 Server Configuration

In the server config file, introduce secured behavior and specify "Message" security mode:

<services>
  <service behaviorConfiguration="SecuredBehavior"  name="SimpleServiceLibrary.SimpleService">
    <endpoint address="mex" binding="mexHttpBinding" name="Mex" contract="IMetadataExchange" />
    <endpoint address="net.msmq://localhost/private/SimpleQueue" binding="netMsmqBinding"
      bindingConfiguration="SimpleMsmqBinding" name="SimpleService" contract="SimpleServiceLibrary.ISimpleService" />
    <host>
      <baseAddresses>
        <add baseAddress="http://localhost:8731/ServerApp/SimpleService/" />
      </baseAddresses>
    </host>
  </service>
</services>
<behaviors>
  <serviceBehaviors>
    <behavior name="SecuredBehavior">
      <serviceThrottling maxConcurrentCalls="1" maxConcurrentInstances="1" maxConcurrentSessions="1">
</serviceThrottling>
      <serviceMetadata httpGetEnabled="True"  />
      <serviceDebug includeExceptionDetailInFaults="True" />
      <serviceCredentials>
        <serviceCertificate findValue="SimpleQueueServer" storeLocation="LocalMachine" storeName="My" x509FindType="FindBySubjectName" />
        <clientCertificate>
          <certificate findValue="SimpleQueueClient" storeLocation="LocalMachine" storeName="My" x509FindType="FindBySubjectName" />
          <authentication certificateValidationMode="None" />
        </clientCertificate>
      </serviceCredentials>
    </behavior>
  </serviceBehaviors>
</behaviors>
<bindings>
  <netMsmqBinding>
    <binding name="SimpleMsmqBinding" exactlyOnce="true" receiveErrorHandling="Fault">
      <security mode="Message">
        <message clientCredentialType="Certificate" />
      </security>
    </binding>
  </netMsmqBinding>
</bindings>

Please pay attention to the netMsmqBinding->binding->exactlyOnce attribute. MSMQ 4.0 seems to be sensitive to this attribute when working with transactional queues, so you may want to make sure it is set to "true" for transactional queues.

3.4.2 Client Configuration

Similar on the client side. Don't forget about DNS identity of the remote endpoint: the client has to make sure it talks to the real "SimpleQueueService".

<client>
  <endpoint address="net.msmq://localhost/private/SimpleQueue"
    behaviorConfiguration="SecuredBehavior" binding="netMsmqBinding"
    bindingConfiguration="SimpleMsmqBinding" contract="SimpleServiceReference.ISimpleService"
    name="SimpleServiceEndpoint">
    <identity>
      <dns value="SimpleQueueServer" />
    </identity>
  </endpoint>
</client>
<behaviors>
  <endpointBehaviors>
    <behavior name="SecuredBehavior">
      <clientCredentials>
        <clientCertificate findValue="SimpleQueueClient" storeLocation="LocalMachine" storeName="My" x509FindType="FindBySubjectName" />
        <serviceCertificate>
          <defaultCertificate findValue="SimpleQueueServer" storeLocation="LocalMachine" storeName="My" x509FindType="FindBySubjectName" />
          <authentication certificateValidationMode="None" />
        </serviceCertificate>
      </clientCredentials>
    </behavior>
  </endpointBehaviors>
</behaviors>
<bindings>
  <netMsmqBinding>
    <binding name="SimpleMsmqBinding" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00"
      sendTimeout="00:01:00" deadLetterQueue="System" durable="true" exactlyOnce="true" maxReceivedMessageSize="65536"
      maxRetryCycles="2" receiveErrorHandling="Fault" receiveRetryCount="5"
      retryCycleDelay="00:30:00" timeToLive="1.00:00:00" useSourceJournal="false" useMsmqTracing="false"
      queueTransferProtocol="Native" maxBufferPoolSize="524288" useActiveDirectory="false">
      <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384" />
      <security mode="Message">
        <message clientCredentialType="Certificate" />
      </security>
    </binding>
  </netMsmqBinding>
</bindings>

3.4.3 Certificate Validation Mode

In our example, we set certificateValidationMode to "None", because it's a test environment and there is no such an authority that would trust our test certificate generated by certmgr.exe. In the production environment, consider changing it to something more secure, like ChainTrust.
Read about certificateValidationMode values: http://msdn.microsoft.com/en-us/library/system.servicemodel.security.x509certificatevalidationmode.aspx
Read about certificate chains: http://msdn.microsoft.com/en-us/library/bb540794(VS.85).aspx

3.4.4 Try it

Run the test framework again. You will see that:

It's also worth testing a deployment with client security turned off or server security turned off (just put binding->security->mode to "None" in the correspondent config file), the messages in this case should not be processed by the server, they will end up in a the "Retry" or the "Dead-letter" sections of the queue.

4. Comments/Issues

This section discusses some known issues and miscellaneous comments.

4.1 Anonymous access to the queue

Potential DoS attack threat.

4.2 Chain Trust

I did not try it with real X.509 certificates, certificateValidationMode="ChainTrust". Can't see any problems with it though.

Appendix A: WCF URL reservation

This section is important if you work in the deployment environment. A URL reservation allows you to restrict who can receive messages from a URL or a set of URLs. If you do not do it properly, your WCF service won't be able to listen on the URL location you specified in <endpoint> element of your config file.
Assumptions:

The procedure is a bit different for older systems (W23K) and new systems (W2K8, Windows 7). For W2K3 systems, use httpcfg.exe utility:

httpcfg.exe set urlacl /u http://+:8731/ServerApp /a "D:(a;;GX;;;NS)"

To verify that this URL is accessible by Network Service, run

httpcfg query urlacl

it will give, among other records, the following information:

URL : http://+:8731/ServerApp/
ACL : D:(A;;GX;;;NS)

See SDDL (Security Descriptor Definition Language) reference for more details on notation.

For W2K8 and Windows 7, use netsh command (it's a part of the OS):

netsh http add urlacl url=http://+:8731/ServerApp/SimpleService user=NetworkService

To verify that this url is accessible by Network Service, run

netsh http show urlacl

it will give, among other records, the following information:

Reserved URL: http://+:8731/ServerApp/
User: NT AUTHORITY\NETWORK SERVICE
Listen: Yes
Delegate: No
SDDL: D:(A;;GX;;;NS)

The following resources can be very helpful when working with httpcfg and netsh utilities:

Configuring HTTP, Nicholas Allen: http://blogs.msdn.com/drnick/archive/2006/04/14/configuring-http.aspx
httpcfg syntax: http://technet.microsoft.com/en-us/library/cc781601.aspx
SDDL syntax: http://msdn.microsoft.com/en-us/library/aa379567(VS.85).aspx
Netsh Commands for HTTP: http://technet.microsoft.com/en-us/library/cc725882.aspx
Configuring HTTP for Windows Vista, Nicholas Allen: http://blogs.msdn.com/drnick/archive/2006/10/16/configuring-http-for-windows-vista.aspx
Configuring HTTP and HTTPS (2003 and 2008): http://msdn.microsoft.com/en-us/library/ms733768.aspx
How to: Replace the WCF URL Reservation with a Restricted Reservation: http://msdn.microsoft.com/en-us/library/dd767317.aspx)

Appendix B: cert.cmd file

@echo off

rem --------------------------------------------------------
rem 32bit vs 64 bit scripting engine

if "%ProgramFiles(x86)%" == "" goto thirtytwobit

set CScriptPath=%windir%\SYSWOW64\CSCRIPT.EXE

goto getparams

:thirtytwobit

set CScriptPath=cscript.exe

rem --------------------------------------------------------
rem Parse parameters


:getparams


if "%1" == "showstore" goto showstore
if "%1" == "cleanstore" goto cleanstore
if "%1" == "cleanfiles" goto cleanfiles
if "%1" == "create" goto create
if "%1" == "setupserver" goto setupserver
if "%1" == "setupclient" goto setupclient
if "%1" == "setupclientandserver" goto setupclientandserver

rem --------------------------------------------------------
rem Help

:usage

echo Simple WCF certificate management utility.
echo .
echo This script assumes the user operates a pair of certificates called
echo SimpleQueueServer and SimpleQueueClient.
echo If you want to have several pairs of client/server
echo (you will need them if you want to run several test/staging/production
echo deployments), you have to replace correspondent names in this script
echo and in your WCF configuration files (client and server).
echo .
echo Create command generates certificates that are valid
echo for 01/01/2000 - 01/01/2039 date range.
echo .
echo Deployed certificates are stored in "My" store at "LocalMachine" profile.
echo .
echo Usage:
echo .
echo cert showstore
echo cert cleanstore (WARNING: deletes ALL certificates from "localMachine My")
echo cert cleanfiles (WARNING: deletes ALL certificate files from current folder)
echo cert create (WARNING: may overwrite existing certificate files)
echo cert setupserver serveraccount (e.g.: cert setupserver NetworkService)
echo cert setupclient clientaccount (e.g.: cert setupclient ASPNET)
echo cert setupclientandserver clientaccount serveraccount (useful for dev box setup)

goto end

: showstore

echo --------------------------------------------------------
echo Display store contents...
certmgr -c -s -r localMachine My

goto end

:cleanstore

echo --------------------------------------------------------
echo Delete all certificates from localMachine store...
certmgr -del -all -s -r LocalMachine My

goto end

:cleanfiles

del *.pfx
del *.pvk
del *.cer

goto end

:create

echo --------------------------------------------------------
echo Create client certificate (click None if you do not want to protect your private key with a password)...
makecert -r -pe -n "CN=SimpleQueueClient" -b 01/01/2000 -e 01/01/2039 -sky exchange SimpleQueueClient.cer -sv SimpleQueueClient.pvk

echo --------------------------------------------------------
echo Merge client public and privat keys on a pfx file ...
pvk2pfx.exe -pvk SimpleQueueClient.pvk -spc SimpleQueueClient.cer -pfx SimpleQueueClient.pfx

echo --------------------------------------------------------
echo Create server certificate (click None if you do not want to protect your private key with a password)...
makecert -r -pe -n "CN=SimpleQueueServer" -b 01/01/2000 -e 01/01/2039 -sky exchange SimpleQueueServer.cer -sv SimpleQueueServer.pvk

echo --------------------------------------------------------
echo Merge server public and privat keys on a pfx file...
pvk2pfx.exe -pvk SimpleQueueServer.pvk -spc SimpleQueueServer.cer -pfx SimpleQueueServer.pfx

echo --------------------------------------------------------
echo Delete private key files...
del *.pvk

goto end

:setupclient

if "%2" == "" goto usage

echo --------------------------------------------------------
echo Add client certificate (public and private keys) from exported file to store...
%CScriptPath% cstore.vbs import -l LM -s My SimpleQueueClient.pfx

echo --------------------------------------------------------
echo Make client key accessible for %2...
for /F "delims=" %%i in ('FindPrivateKey.exe My localMachine -n CN^=SimpleQueueClient -a') do set PRIVATE_KEY_FILE=%%i
echo Y|cacls.exe "%PRIVATE_KEY_FILE%" /E /G %2:R

echo --------------------------------------------------------
echo Add server certificate (public key only) from exported file to store...
certmgr -add SimpleQueueServer.cer -r localMachine -c -n SimpleQueueServer -s My

echo --------------------------------------------------------
echo Display store contents...
certmgr -c -s -r localMachine My


goto end

:setupserver

if "%2" == "" goto usage

echo --------------------------------------------------------
echo Add server certificate (public and private keys) from exported file to store...
%CScriptPath% cstore.vbs import -l LM -s My SimpleQueueServer.pfx


echo --------------------------------------------------------
echo Make server key accessible for %2...
for /F "delims=" %%i in ('FindPrivateKey.exe My localMachine -n CN^=SimpleQueueServer -a') do set PRIVATE_KEY_FILE=%%i
echo Y|cacls.exe "%PRIVATE_KEY_FILE%" /E /G %2:R

echo --------------------------------------------------------
echo Add client certificate (public key only) from exported file to store...
certmgr -add SimpleQueueClient.cer -r localMachine -c -n SimpleQueueClient -s My

goto end

:setupclientandserver

if "%2" == "" goto usage
if "%3" == "" goto usage

echo --------------------------------------------------------
echo Add client certificate (public and private keys) from exported file to store...
%CScriptPath% cstore.vbs import -l LM -s My SimpleQueueClient.pfx

echo --------------------------------------------------------
echo Make client key accessible for %2...
for /F "delims=" %%i in ('FindPrivateKey.exe My localMachine -n CN^=SimpleQueueClient -a') do set PRIVATE_KEY_FILE=%%i
echo Y|cacls.exe "%PRIVATE_KEY_FILE%" /E /G %2:R

echo --------------------------------------------------------
echo Add server certificate (public and private keys) from exported file to store...
%CScriptPath% cstore.vbs import -l LM -s My SimpleQueueServer.pfx

echo --------------------------------------------------------
echo Make server key accessible for %3...
for /F "delims=" %%i in ('FindPrivateKey.exe My localMachine -n CN^=SimpleQueueServer -a') do set PRIVATE_KEY_FILE=%%i
echo Y|cacls.exe "%PRIVATE_KEY_FILE%" /E /G %3:R

:end

rem ...............Samples.....................

rem echo --------------------------------------------------------
rem echo Save cert to TrustedPeople...
rem certmgr.exe -add -r localMachine -s My -c -n SimpleQueueServer -r LocalMachine -s TrustedPeople

rem echo --------------------------------------------------------
rem echo Export client certificate...
rem certmgr -put -r CurrentUser -s My -c -n SimpleClientQueue SimpleClientQueue.cer

rem Unfortunately, certmgr doesn't work properly with pfx, so we have to use capicom.dll (cstore.vbs)
rem certmgr -add SimpleQueueServer.pfx -r localMachine -c -n SimpleQueueServer -s My
rem %CScriptPath% cstore.vbs import -l LM -s My SimpleQueueServer.pfx

Appendix C: sample code and tools

Download sample source code and tools.