Part 2, containing detailed explanations of the implementation can be found here.
Table of Contents
Introduction
With the appearance of Windows Communication Foundation, building Service Oriented Applications became easier than ever. And lots of articles poured in with extensions for special cases. Even so, there are still situations left untreated. Like the following I had to resolve:
- Client-server application – http protocol – NO IIS
- Authentication – user/password from a database – NO SSL/X509 certificate
- Authorization – roles from a database
- Encryption for the credentials (with option for the entire request/response)
- Compression for both the request and response
Logical Solution
For each of the above requirements/restrictions, we have:
- No IIS – We need our own http server – can be easily done by WCF in a few lines of code - no need to insist on this.
- User/Password authentication – This is easily done by default, but an X509 certificate is required. Therefore we need our own mechanism: we’ll add the credentials to the header of the message and encrypt them (or the entire message).
- Encryption – We’ll use an asymmetric algorithm[1] (RSA) with public/private keys. Usually, it is needed that the server and the client have their own set of public/private keys to encrypt both the request and the response, but we’ll use an artifice to avoid the client set of keys.
With RSA, only a small amount of data can be encrypted (for 2048 bit encryption, only 128 bytes of data). Therefore it is used to encrypt a random generated password that will be used by a symmetric algorithm[2] (AES) to encrypt the message. The server decrypts with its private key, the password of the client, with that password decrypts the message. And using the same password encrypts the response message.
- Additional security for the credentials – Even if the credentials are encrypted, a listener can get those encrypted credentials and launch a new request, therefore we’ll add an expiration date for the message.
Also, during the time till expiration, the password is banned - it cannot be used by another message.
This way, if a copy of the message is sent right away, it will be rejected because the password is banned; if it is sent after the password ban expired, then the authentication token expired also and the message will be rejected. Actually, the password will act like a nonce. - Compression – we’ll use gzip compression/decompression just before sending/receiving the message and the response.
Now, let’s see how the above generic considerations look in the client-server message flow:
- Server starts; a new RSA key is generated (or loaded from the disk).
- Client starts; it asks the server for the public key and time; based on server time and client time, it will calculate the client-server-timespan.
- Client prepares the request message
- The credentials are added to the message header; the
Credentials
token will have User/Password/Expires
properties; the Expires
will be calculated as client time + client-server-timespan + a few seconds; - The message (or only the credentials part) is encrypted; more exactly:
- A random key is generated and saved along with the message id;
- The message (or the credentials part) is encrypted with the AES algorithm using the previous generated key;
- The AES’ key is encrypted using the server’s RSA public key and added to the encrypted message
- The message is compressed
- Server receives the request message
- The message is decompressed
- The message is decrypted; more exactly:
- The client AES’ key is retrieved by decrypting using the server’s private key;
- If the key is in the ban list, a
SecurityException
is thrown; if it isn't, it is added with the ban's expiration date. - The message is decrypted using the client key;
- The client’s key will be saved along with the message id – it will be needed to encrypt the response with the same id;
- The credentials are extracted from the message; the
Expires
is compared with server’s current time and if it is bigger, an AuthenticationException
is thrown; - Authentication – The credentials are verified against a database;
- Authorization – The roles of the authenticated user are retrieved from a database/cache.
- Server prepares the response message
- The message is encrypted; more exactly the message will be encrypted (AES) using the client’s key saved during decryption of the request;
- The message is compressed
- Client receives the response message
- The message is decompressed.
- The message is decrypted using the key saved during encryption (3.b.)
How to extend WCF to implement the above flow
On the client, for adding the credentials, we’ll use a <a href="http://msdn.microsoft.com/en-us/library/system.servicemodel.configuration.behaviorextensionelement.aspx">BehaviorExtensionElement</a>
that implements <a href="http://msdn.microsoft.com/en-us/library/system.servicemodel.dispatcher.iclientmessageinspector.aspx">IClientMessageInspector</a>
which has a BeforeSendRequest
method (see implementation here).
On the server, for authentication, we need a class to implement <a href="http://msdn.microsoft.com/en-us/library/system.security.principal.iidentity.aspx">IIdentity</a>
and a class derived from <a href="http://msdn.microsoft.com/en-us/library/system.servicemodel.serviceauthenticationmanager.aspx">ServiceAuthenticationManager</a>
and to override the Authenticate
method[3] (see implementation here).
For authorization, we need classes that implement <a href="http://msdn.microsoft.com/en-us/library/system.security.principal.iprincipal.aspx">IPrincipal</a>
and <a href="http://msdn.microsoft.com/en-us/library/system.identitymodel.policy.iauthorizationpolicy.aspx">IAuthorizationPolicy</a>
respectively (see implementation here).
On both client and server, for cryptography and compression, we’ll need classes derived from <a href="http://msdn.microsoft.com/en-us/library/system.servicemodel.channels.messageencoder.aspx">MessageEncoder</a>
[4], <a href="http://msdn.microsoft.com/en-us/library/system.servicemodel.channels.messageencoderfactory.aspx">MessageEncoderFactory</a>
[13], <a href="http://msdn.microsoft.com/en-us/library/system.servicemodel.channels.messageencodingbindingelement.aspx">MessageEncodingBindingElement</a>
, <a href="http://msdn.microsoft.com/en-us/library/system.servicemodel.configuration.bindingelementextensionelement.aspx">BindingElementExtensionElement</a>
; the first class derived from <a href="http://msdn.microsoft.com/en-us/library/system.servicemodel.channels.messageencoder.aspx">MessageEncoder</a>
will do the actual encryption/decryption and compression/decompression; the class derived from <a href="http://msdn.microsoft.com/en-us/library/system.servicemodel.configuration.bindingelementextensionelement.aspx">BindingElementExtensionElement</a>
is our entry point from the config; it will use a <a href="http://msdn.microsoft.com/en-us/library/system.servicemodel.channels.messageencodingbindingelement.aspx">MessageEncodingBindingElement</a>
that will use a <a href="http://msdn.microsoft.com/en-us/library/system.servicemodel.channels.messageencoderfactory.aspx">MessageEncoderFactory</a>
that will use the <a href="http://msdn.microsoft.com/en-us/library/system.servicemodel.channels.messageencoder.aspx">MessageEncoder</a>
(see implementation here).
While compression is the same for the server and the client, the encryption differs on client from the server therefore we’ll have the above classes defined for each of them (of course, what is the same will be put in a parent class from which both the client and the server versions will derive.
You may think that a better solution would’ve been to use the message encoder only for the compression and to use a formatter to encrypt the message, but it won’t work: authentication happens before de-formatting on the server so we need to have the credentials decrypted by then.
Implementation
We’ll have 3 projects – Client, Server and Common (common library for both the server and the client).
And the following classes - all resulting naturally from the above arguments: Credentials
, ServerInfo
, a ClientCriptographer
and a ServerCryptographer
, a ClientEncoder
and a ServerEncoder
.
The following diagram contains the main classes:
(Click on the diagram to enlarge)
(Click on the diagram to enlarge)
Due to its size, the implementation will be presented in detail in Part 2 of this article.
Security Considerations
Trying to decipher the credentials, it’s complicated – the credentials token changes with every request as the Expires
property changes – and more important, the AES key is changed with every request.
It would be easier to try to decipher server’s RSA key - but not easy enough. Everything is in fact reduced to the amount of the processing power that the cryptanalyst has at his disposal. Since he doesn’t have "the universal code breaker"[10] working for him, breaking the RSA key may take month, even years – but it is doable. However, this can be useless if the client credentials and the RSA key are changed often; (already the RSA key it is changed every time the server is restarted).
The real problem comes from an attacker that can control the conversation between client and server and can impersonate them; like the man in the middle attack[15]. To avoid this kind of attack, you need to use SSL that authenticates the server using a mutually trusted certification authority.
How to Use the Code
Just search the entire solution for "example"; there you can do your modifications/addings. Or, based on the example model, add your own service.
You may be also interested in changing the contentEncryption
and contentCompression
properties from the ClientMessageEncoding
and ServerMeesageEncoding
from the configuration files.
Extensions Ideas
Most of them are related to changing the encoding:
- to allow encrypting only the response or only the request or only parts of them;
- to allow compressing only the response or the request;
- to change the text encoding to binary encoding (which means changing the XML encryption to a custom binary encryption);
- to change the order of encryption and compression – encryption followed by compression is not always the best solution.
Notes
- The solution uses .NET 4.0.
- Please ignore the warnings related to the source control (TFS).
- You may need to re-add some of the references:
System.Configuration
, System.ServiceModel
, System.IdentityModel
, System.Security
, System.Runtime.Serialization
, Ionic.Zip (the last is in the Common project; the others come with .NET 4). - The references are for both parts of the article and for the attached code (comments with numbers).
- You need administrative rights to start the server.
- To use Microsoft’s implementation of zip and remove the need for Ionic.Zip.dll, just change in the Common Project, Encoding.cs the “
using Ionic.Zlib;
” with “using System.IO.Compression;
”; then you can delete the reference and the DLL. - To avoid duplication, the code is attached only to the first part of the article.
- If you want to directly test the server.exe and the client.exe from the attached zip, you need to unblock first server.exe.config and client.exe.config (right click, properties, unblock); otherwise you'll get a configuration error.
References
[1] RSACryptoServiceProvider Info[2] AesCryptoServiceProvider Info [3] Custom WCF authentication [4] Custom Message Encoder: Custom Text Encoder [5] Cryptography Helper [6] How to Get the AES Encryption Key from a RSA+AES Encrypted XML [7] WCF GZip Compression Bug [8] How to: Encrypt XML Elements with Asymmetric Keys [9] How to: Encrypt XML Elements with Symmetric Keys [10] The Universal Code Breaker [11] Wcf ClearUsernameBinding [12] Microsoft samples [13] Microsoft code - Encoder/Factory [14] Resolving XmlDictionaryReaderQuotas Error for WCF Compression using GZipEncoder with Custom Binding [15] Man in the middle attack
History
2011-03-08 Version 1.0.0
2011-03-15 Version 1.0.1
2011-03-17 Version 1.1.0
- Updated Security Considerations with info about the man in the middle attack
- Updated client-server flow - 4.b. to add the random client's password to the server's ban list
- Updated code to treat the modified client server flow
2011-03-23 Version 1.1.1