Table of Contents
This part presents in detail the implementation that due to its size was not treated in part 1.
Overview
We have 3 parts that will result in 3 projects – Client
, Server
and Common
(common library for both the server and the client).
First, we take a quick look at the classes and I’ll explain what they do – though I think it is pretty obvious.
(Click on the diagram to enlarge)
Let’s start with the common part.
The ServerInfo
contains the date/time and the public key of the server.
The Credentials
contains UserName/Password/Expires
properties that get serialized and sent to the server; the others (about date/time) are static
properties initialized at the beginning from the server info and are used to set the Expires
property just before the request.
Let’s move to the server at AppServer
static
class; its role is to create/start/stop the service that will serve to clients the ServerInfo
.
Now on the client, we have AppClient
static
class; its role is to get the ServerInfo
; it also hosts the client’s credentials.
Authentication
We start on the client where we add the Credentials to the message header; we do this by using a BehaviorExtensionElement that also implements IClientMessageInspector, like this:
public class ClientMessageInspector : BehaviorExtensionElement,
IClientMessageInspector, IEndpointBehavior
{
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
request.Headers.Add(AppClient.Credentials.ToMessageHeader());
return null;
}
}
and in the configuration file (App.config):
<behaviorExtensions>
<add name="ChallengeClientMessageInspector"
type="Challenge.Client.ClientMessageInspector, Client" />
</behaviorExtensions>
This is followed by the encoding mechanism where encryption and compression occurs, but we’ll talk about this later.
Now on the server: we need to derive a class from <a href="http://msdn.microsoft.com/en-us/library/system.servicemodel.serviceauthenticationmanager.aspx">ServiceAuthenticationManager</a>
and override the Authenticate
method[3]; here if the CheckCredentials
passes without any exception being thrown, then we authenticate the user from the credentials.
public class ChallengeAuthenticationManager : ServiceAuthenticationManager
{
public override ReadOnlyCollection<IAuthorizationPolicy>
Authenticate(ReadOnlyCollection<IAuthorizationPolicy> authPolicy,
Uri listenUri, ref Message message)
{
Credentials credentials = Credentials.FromMessageHeader(message);
CheckCredentials(credentials);
ChallengeIdentity identity = new ChallengeIdentity(credentials.UserName);
IPrincipal user = new ChallengePrincipal(identity);
message.Properties["Principal"] = user;
return authPolicy;
}
public void CheckCredentials(Credentials credentials)
{
if (credentials.Expires < DateTime.Now)
throw new AuthenticationException("Credentials expired!");
}
}
<behavior name="ChallengeBehavior">
<serviceAuthenticationManager
serviceAuthenticationManagerType=
"Challenge.Server.ChallengeAuthenticationManager,
Server"/>
</behavior>
Now let’s explain the ChallengeIdentity
and ChallengePrincipal
: first implements IIdentity and the later IPrincipal.
Both are extremely simple to implement; however I’ll insist below on the ChallengePrincipal
because in it lays our authorization.
Authorization
With authorization, we can establish which of the authenticated users are allowed to execute the operation contract and the permission looks like this:
[PrincipalPermission(SecurityAction.Demand, Role = "Admin")]
public int Sum(int a, int b)
{
return a + b;
}
The authenticated user will be allowed to execute the above method only if he is in the “Admin” role – this is checked using the IsInRole
method:
public class ChallengePrincipal: IPrincipal
{
IIdentity identity;
string[] roles = null;
public bool IsInRole(string role)
{
EnsureRoles();
return roles != null ? roles.Contains(role) : false;
}
protected virtual void EnsureRoles()
{
roles = new string[] { "User", "Admin", "Manager" };
}
}
In order to make the authorization work, we need to create our own authorization policy (and specify it in the configuration file) like this:
public class ChallengeAuthorizationPolicy : IAuthorizationPolicy
{
public bool Evaluate(EvaluationContext evaluationContext, ref object state)
{
IPrincipal user = OperationContext.Current.IncomingMessageProperties
["Principal"] as IPrincipal;
evaluationContext.Properties["Principal"] = user;
evaluationContext.Properties["Identities"] = new List<IIdentity>
{ user.Identity };
return false;
}
}
<behavior name="ChallengeBehavior">
<serviceAuthorization principalPermissionMode="Custom" >
<authorizationPolicies>
<add policyType='Challenge.Server.ChallengeAuthorizationPolicy, Server' />
</authorizationPolicies>
</serviceAuthorization>
</behavior>
Encryption
Encryption/decryption is provided by common Cryptographer
/ClientCryptographer
/ServerCryptographer
; a separate cryptographer for server and client is needed because they do different encryption/decryption.
Client - Encrypt Request
- Get the id of the message, associate it with the key and keep them until the response from the server comes – we’ll need that key to decrypt the response from the server.
- Get the element to encrypt (the
Credentials
element if only credentials are to be encrypted or the first node if the entire message will be encrypted) and encrypt that element. - Encrypt the AES key with the
public
key[2] of the server and add it to the encrypted node (using the name KeyElementName
– which is a constant for both the server and the client). - Set the id of the encrypted element to the id of the message (to know the id of the message before decryption).
- Replace the original node with the encrypted node.
public static void Encrypt(XmlDocument xmlDoc, string elementToEncrypt)
{
XmlNodeList elementsToEncrypt = xmlDoc.GetElementsByTagName(elementToEncrypt);
if (elementsToEncrypt.Count == 0)
return;
AesCryptoServiceProvider aesServiceProvider =
new AesCryptoServiceProvider();
aesServiceProvider.KeySize = 256;
aesServiceProvider.GenerateKey();
XmlNode idNode = xmlDoc.GetElementsByTagName("a:MessageID")[0];
string id = idNode.InnerText;
AesKeys.Add(id, aesServiceProvider.Key);
XmlElement xmlElementToEncrypt = (XmlElement)elementsToEncrypt[0];
EncryptedXml encryptedXml = new EncryptedXml();
byte[] encryptedElement = encryptedXml.EncryptData
(xmlElementToEncrypt, aesServiceProvider, Content);
EncryptedData encryptedData = new EncryptedData();
encryptedData.Type = EncryptedXml.XmlEncElementUrl;
encryptedData.EncryptionMethod =
new EncryptionMethod(EncryptedXml.XmlEncAES256Url);
EncryptedKey encryptedKey = new EncryptedKey();
encryptedKey.CipherData = new CipherData
(EncryptedXml.EncryptKey(aesServiceProvider.Key, RsaServiceProvider, Content));
encryptedKey.EncryptionMethod =
new EncryptionMethod(EncryptedXml.XmlEncRSA15Url);
encryptedData.KeyInfo = new KeyInfo();
encryptedKey.KeyInfo.AddClause(new KeyInfoName(KeyElementName));
encryptedData.KeyInfo.AddClause(new KeyInfoEncryptedKey(encryptedKey));
encryptedData.CipherData.CipherValue = encryptedElement;
encryptedData.Id = id;
EncryptedXml.ReplaceElement(xmlElementToEncrypt, encryptedData, Content);
}
Server - Decrypt Request
- Consider the incoming document as an encrypted one;
- Link the
KeyElementName
(which as I said before is constant for both the client and server) with the private key of the server that will be used to decrypt the client AES key; - Associate the id of the message to the key – this is needed to find the client key when encrypting the response;
- Add the password to the ban list (or, if exists, throw a
SecurityException
); - Decrypt the document.
public static void Decrypt(XmlDocument xmlDoc)
{
XmlNodeList encryptedElements = xmlDoc.GetElementsByTagName("EncryptedData");
if (encryptedElements.Count == 0)
return;
EncryptedXml encryptedXml = new EncryptedXml(xmlDoc);
encryptedXml.AddKeyNameMapping(KeyElementName, RsaServiceProvider);
EncryptedData eData = new EncryptedData();
XmlElement encryptedElement = (XmlElement)encryptedElements[0];
eData.LoadXml(encryptedElement);
SymmetricAlgorithm a = encryptedXml.GetDecryptionKey
(eData, eData.EncryptionMethod.KeyAlgorithm);
AesKeys.Add(eData.Id, a.Key);
string keyHash = a.Key.ComputeHash();
if (AesBannedKeys.ContainsKey(keyHash))
throw new SecurityException("Password reuse before ban expiration!");
else
AesBannedKeys.Add(keyHash, DateTime.Now.AddMilliseconds
(AesBannedKeysExpiresTimeSpan));
encryptedXml.DecryptDocument();
}
Server - Encrypt Response
- Find the element to encrypt.
- Get the key based of the id of the message (
RelatesTo
element) that was saved from the request. - Encrypt the message.
- Set the id of the message to the encrypted data (so that on the client to know which key to use – if only synchronous requests would be sent, then we’ll need just to use the last key, but we have to take both situations under consideration).
- Replace the original element with the encrypted one.
public static void Encrypt(XmlDocument xmlDoc, string elementToEncrypt)
{
XmlNodeList elementsToEncrypt = xmlDoc.GetElementsByTagName(elementToEncrypt);
if (elementsToEncrypt.Count == 0)
return;
AesCryptoServiceProvider aesServiceProvider = new AesCryptoServiceProvider();
XmlNode idNode = xmlDoc.GetElementsByTagName("a:RelatesTo")[0];
string id = "";
if (idNode != null)
{
id = idNode.InnerText;
}
if (AesKeys.ContainsKey(id))
{
aesServiceProvider.Key = AesKeys[id];
}
XmlElement xmlElementToEncrypt = (XmlElement)elementsToEncrypt[0];
EncryptedXml encryptedXml = new EncryptedXml();
byte[] encryptedElement = encryptedXml.EncryptData
(xmlElementToEncrypt, aesServiceProvider, Content);
EncryptedData encryptedData = new EncryptedData();
encryptedData.Type = EncryptedXml.XmlEncElementUrl;
encryptedData.EncryptionMethod = new EncryptionMethod
(EncryptedXml.XmlEncAES256Url);
encryptedData.CipherData.CipherValue = encryptedElement;
encryptedData.Id = id;
EncryptedXml.ReplaceElement(xmlElementToEncrypt, encryptedData, Content);
}
Client - Decrypt Response
- Find the encrypted element and load it as an encrypted data.
- Get the key to for the AES algorithm based on the id of the encrypted data.
- Decrypt the encrypted element and replace it in the document.
public static void Decrypt(XmlDocument xmlDoc)
{
XmlNodeList encryptedElements = xmlDoc.GetElementsByTagName("EncryptedData");
if(encryptedElements.Count == 0)
return;
XmlElement encryptedElement = (XmlElement)encryptedElements[0];
EncryptedData encryptedData = new EncryptedData();
encryptedData.LoadXml(encryptedElement);
AesCryptoServiceProvider aesServiceProvider = new AesCryptoServiceProvider();
if (AesKeys.ContainsKey(encryptedData.Id))
{
aesServiceProvider.Key = AesKeys[encryptedData.Id];
}
EncryptedXml encryptedXml = new EncryptedXml();
encryptedXml.ReplaceData(encryptedElement,
encryptedXml.DecryptData(encryptedData, aesServiceProvider));
}
Since we want to do this in a message encoder, we don’t have the message as an XML document, but as an array segment; therefore, for client and server we need to do encryption on array segments (actually convert the array segments to XML documents and then apply the above methods).
public static ArraySegment<byte> EncryptBuffer(ArraySegment<byte> buffer,
BufferManager bufferManager, int messageOffset,
string elementToEncrypt = "s:Envelope")
{
byte[] bufferedBytes;
byte[] encryptedBytes;
XmlDocument xmlDoc = new XmlDocument();
using (MemoryStream memoryStream = new MemoryStream
(buffer.Array, buffer.Offset, buffer.Count))
{
xmlDoc.Load(memoryStream);
}
ClientCryptographer.Encrypt(xmlDoc, elementToEncrypt);
encryptedBytes = Encoding.UTF8.GetBytes(xmlDoc.OuterXml);
bufferedBytes = bufferManager.TakeBuffer(encryptedBytes.Length);
Array.Copy(encryptedBytes, 0, bufferedBytes, 0, encryptedBytes.Length);
bufferManager.ReturnBuffer(buffer.Array);
ArraySegment<byte> byteArray = new ArraySegment<byte>
(bufferedBytes, messageOffset, encryptedBytes.Length);
return byteArray;
}
Create a new AesCryptoServiceProvider
[1] and generate a new key.
Compression
For compression/decompression, we will use Microsoft’s example; the decompression method is identical; I changed the compression because of a serious bug[7].
public static ArraySegment<byte> CompressBuffer
(ArraySegment<byte> buffer, BufferManager bufferManager, int messageOffset)
{
byte[] bufferedBytes, compressedBytes;
using (MemoryStream memoryStream = new MemoryStream())
{
memoryStream.Write(buffer.Array, 0, messageOffset);
using (GZipStream gzStream = new GZipStream
(memoryStream, CompressionMode.Compress, true))
{
gzStream.Write(buffer.Array, messageOffset, buffer.Count);
}
compressedBytes = memoryStream.ToArray();
bufferedBytes = bufferManager.TakeBuffer(compressedBytes.Length);
Array.Copy(compressedBytes, 0, bufferedBytes, 0, compressedBytes.Length);
bufferManager.ReturnBuffer(buffer.Array);
}
ArraySegment<byte> byteArray =
new ArraySegment<byte>(bufferedBytes, messageOffset, compressedBytes.Length);
return byteArray;
}
Encoding
This might be not so easy to understand by just following the attached code; so I’ll present it with a few changes to make it easier to understand.
First, I’ll show only the encoding done for the client (there is no point in showing the server because it is similar); and second - I’m combining the common with the client (because actually all started this way – thinking at the client, then at the server and after that, at their identical parts and thus the “common” project came into being).
First, we have the ClientEncoder
based on the abstract
class MessageEncoder with the most important methods WriteMessage
and ReadMessage
; here we use the methods presented in the Encryption and Compression chapters; the ContentCompression
and ContentEncryption
properties are initialized by the class that creates the encoder and are actually a propagation of the contentCompression
and contentEncryption
attributes from the configuration file (a little later, we’ll see how this is retrieved from the App.config).
public class ClientMessageEncoder : MessageEncoder
{
public override ArraySegment<byte> WriteMessage(Message message,
int maxMessageSize, BufferManager bufferManager, int messageOffset)
{
ArraySegment<byte> buffer = innerEncoder.WriteMessage
(message, maxMessageSize, bufferManager, messageOffset);
switch (ContentEncryption)
{
case ContentEncryptionType.All:
{
buffer = ClientCryptographer.EncryptBuffer(buffer,
bufferManager, messageOffset);
break;
}
case ContentEncryptionType.Credentials:
{
buffer = ClientCryptographer.EncryptBuffer(buffer,
bufferManager, messageOffset,
ContentEncryptionType.Credentials.ToString());
break;
}
}
if (ContentCompression != ContentCompressionType.None)
buffer = CompressBuffer(buffer, bufferManager, messageOffset);
return buffer;
}
public override Message ReadMessage(ArraySegment<byte> buffer,
BufferManager bufferManager, string contentType)
{
ArraySegment<byte> workingBuffer = buffer;
if (ContentCompression != ContentCompressionType.None)
buffer = DecompressBuffer(buffer, bufferManager);
if (ContentEncryption != ContentEncryptionType.None)
buffer = ClientCryptographer.DecryptBuffer(buffer, bufferManager);
Message returnMessage = innerEncoder.ReadMessage(buffer, bufferManager);
returnMessage.Properties.Encoder = this;
return returnMessage;
}
}
The ClientMessageEncoder
is used by a factory encoder:
public class ClientMessageEncoderFactory : MessageEncoderFactory
{
MessageEncoder encoder;
public ClientMessageEncoderFactory(MessageEncoderFactory messageEncoderFactory)
{
encoder = new ClientMessageEncoder(messageEncoderFactory.Encoder);
}
}
The ClientMessageEncoderFactory
is used by the binding element in the CreateMessageEncoderFactory
method:
public class ClientMessageEncodingBindingElement :
MessageEncodingBindingElement
{
public override IChannelFactory<TChannel>
BuildChannelFactory<TChannel>(BindingContext context)
{
context.BindingParameters.Add(this);
var property = GetProperty<XmlDictionaryReaderQuotas>(context);
property.MaxStringContentLength = Int32.MaxValue;
return context.BuildInnerChannelFactory<TChannel>();
}
public override MessageEncoderFactory CreateMessageEncoderFactory()
{
ClientMessageEncoderFactory factory =
new ClientMessageEncoderFactory
(innerBindingElement.CreateMessageEncoderFactory());
ClientMessageEncoder encoder = factory.Encoder as ClientMessageEncoder;
encoder.ContentCompression = ContentCompression;
encoder.ContentEncryption = ContentEncryption;
return factory;
}
}
And the above binding element it is used by the extension element; here we can get attributes from the configuration (like contentEncryption
) and from here we propagate them to the message encoder.
public class ClientMessageEncodingElement : BindingElementExtensionElement
{
[ConfigurationProperty("contentEncryption", DefaultValue = "Credentials")]
public string ContentEncryption
{
get { return (string)base["contentEncryption"];}
set { base["contentEncryption"] = value; }
}
protected override BindingElement CreateBindingElement()
{
ClientMessageEncodingBindingElement bindingElement =
new ClientMessageEncodingBindingElement();
this.ApplyConfiguration(bindingElement);
return bindingElement;
}
}
And this is how the config should look like:
<extensions>
<bindingElementExtensions>
<add name="ClientMessageEncoding"
type="Challenge.Client.ClientMessageEncodingElement, Client" />
</bindingElementExtensions>
</extensions>
...
<bindings>
<customBinding>
<binding name="ChallengeMessageEncoding">
<ClientMessageEncoding contentEncryption="All" contentCompression="GZip" />
<httpTransport/>
</binding>
</customBinding>
</bindings>
Now, that you know "how it is made", you can start using the code and then implement extensions.
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 the 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 test directly 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 - Initial post
- 2011-03-18 Version 1.1.0 - Added the code with the password ban list
- 2011-03-24 Version 1.1.1 - Small text changes