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.
The goal is to implement a secured WCF communication based on MSMQ under the following requirements/assumptions/recommendations.
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.
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.
Windows shell script that uses some of the utilities listed below and facilitates certificate management.
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:
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.
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.
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.
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.
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
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).
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.
Let's create a simple WCF service test framework
Let's use msmq instead of http as a transport (using MSMQ 3.0 on W2K3). No security for now.
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.
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>
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>
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.
This section discusses the workflow for the certificate management on the client (HXAPI web service) and server (Queue Service) machines.
Certificate installation procedure begins with actual certificate generation. This section describes test and production certificate generation separately.
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.
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.
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.
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:
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:
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.
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.
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>
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
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.
This section discusses some known issues and miscellaneous comments.
Potential DoS attack threat.
I did not try it with real X.509 certificates, certificateValidationMode="ChainTrust". Can't see any problems with it though.
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)
@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