Return to HomePage



Encrypting and Signing a cookie for use by an external web application (C#)


Applies to

* ASP.NET 2.0
* C#


Summary

The purpose of this code snippet is to illustrate how to securely distribute session data to an external trusted web application when there exists no common server-side session state. It includes topics such as generating an HMAC, inclusion of timestamp and the encryption of session data.

Objectives

* Provide confidentiality of application cookies while in transit even over insecure communication channels, ensuring that they are not exposed to unauthorized users.
* Provide controls to mitigate session replay
* Provide controls to protect against information disclosure
* Provide controls which validate message integrity
* Protect against users' ability to tamper with cookie parameters which may impact the business logic, authentication or authorization context or overall data integrity through HMAC message signing
* Ensure the secure storage of a shared secret encryption key and HMAC signing key through use of the Data Protection API (DPAPI)

Scenarios

* Web application needs to distribute some details within the session state to another application (e.g. Single sign-on, distributed web server architecture)
* Web applications are deployed within an infrastructure where it isn't feasible to share session state through a common server-side data store.
* A shared symmetric key and HMAC signing key are pre-established either out of band or via some other secure communication channel.
* Developers wish to avoid users from tampering with cookie parameters which may impact the business logic, authentication or authorization context or overall data integrity
* Securely distributing session state to an external application
* Providing confidentiality through cookie encryption
* Providing message integrity through HMAC code embedded in crypto blob
* Mitigate session replay through use of a session timeout mechanism
* A user must retrieve data distributed across multiple web servers, each which serve a different purpose, e.g. customer portal, reporting server, etc.

Solution Example

DPAPI Utility Class for retrieval and storage of keys

public class DPAPIUtil
{
		    		private string [registryKeyName] = "ACMEWebApplication";
		    		private string [registryEncValueName] = "symmetrickey";
		    		private string [registryHMACValueName] = "hmackey";
	

		    		string [fullRegistryKeyPath] = "";
	

		    	public [DPAPIUtil()]
	
{
		        		[fullRegistryKeyPath] = @"HKEY_CURRENT_USER\" + [registryKeyName;]
		    	}
	

		    public void StoreHMACKey(byte[] val)
		        {
		            // Encrypt the HMAC signing key using the DPAPI [ProtectedData] class.
		            // 
		            // We're using the [CurrentUser] scope instead of the [MachineKey] scope
		            // so that other, potentially malicious applications cannot access
		            // this key in the registry and decrypt.
	

		            byte[] [encryptedValBytes] = [ProtectedData.Protect(val,] null,
		                [DataProtectionScope.CurrentUser);]
	

		            // Create a security context for a new key that we will use to store our encrypted connection string.
		            // The security context will restrict access to only our user.
		            string user = [Environment.UserDomainName] + "\\" + [Environment.UserName;]
		            [RegistrySecurity] security = new [RegistrySecurity();]
		            [RegistryAccessRule] rule = new [RegistryAccessRule(user,]
		                            [RegistryRights.FullControl,]
		                            [InheritanceFlags.ContainerInherit,]
		                            [PropagationFlags.None,]
		                            [AccessControlType.Allow);]
		            [security.AddAccessRule(rule);]
	

		            // Actually create the new registry key and apply the security context we just came up with.
		            [Registry.CurrentUser.CreateSubKey(registryKeyName,]
		                            [RegistryKeyPermissionCheck.ReadWriteSubTree,]
		                            security);
	

		            // Write the encrypted string into the registry
		            [Registry.SetValue(fullRegistryKeyPath,] [registryHMACValueName,] [encryptedValBytes);]
		          }
	

		    public void StoreSymmKey(byte[] val)
		    {
		        // Encrypt the shared secret key using the DPAPI [ProtectedData] class.
		        // 
		        // We're using the [CurrentUser] scope instead of the [MachineKey] scope
		        // so that other, potentially malicious applications cannot access
		        // this key in the registry and decrypt.
	

		        byte[] [encryptedValBytes] = [ProtectedData.Protect(val,] null,
		            [DataProtectionScope.CurrentUser);]
		        // Create a security context for a new key that we will use to store our shared secret key.
		        // The security context will restrict access to only our user.
		        string user = [Environment.UserDomainName] + "\\" + [Environment.UserName;]
		        [RegistrySecurity] security = new [RegistrySecurity();]
		        [RegistryAccessRule] rule = new [RegistryAccessRule(user,]
		                        [RegistryRights.FullControl,]
		                        [InheritanceFlags.ContainerInherit,]
		                        [PropagationFlags.None,]
		                        [AccessControlType.Allow);]
		        [security.AddAccessRule(rule);]
	

		        // Actually create the new registry key and apply the security context we just came up with.
		        [Registry.CurrentUser.CreateSubKey(registryKeyName,]
		                        [RegistryKeyPermissionCheck.ReadWriteSubTree,]
		                        security);
	

		        // Write the encrypted string into the registry
		        [Registry.SetValue(fullRegistryKeyPath,] [registryEncValueName,] [encryptedValBytes);]
		    }
	

		        public byte[] [RetrieveHMACKey()]
		        {
		            // Read the encrypted hmac signing key value from the registry 
		            byte[] [encryptedValBytes] = [Registry.GetValue(fullRegistryKeyPath,] [registryHMACValueName,] null) as byte[];
	

		            // Decrypt the encrypted bytes using DPAPI and return
		            byte[] [decryptedValBytes] = [ProtectedData.Unprotect(encryptedValBytes,]
		                null,
		                [DataProtectionScope.CurrentUser);]
		            return [decryptedValBytes;]
	
}

public byte[] RetrieveSymmKey()
{
		        		// Read the encrypted symmetric key value from the registry 
		        		byte[] [encryptedValBytes] = [Registry.GetValue(fullRegistryKeyPath,] [registryEncValueName,] null) as byte[];
	

		        		// Decrypt the encrypted bytes using DPAPI and return
		        		byte[] [decryptedValBytes] = [ProtectedData.Unprotect(encryptedValBytes,]
		            	null,
		            	[DataProtectionScope.CurrentUser);]
		        	return [decryptedValBytes;]
		    		}
	
}


Setting the encrypted and signed key in our cookie

Establish the cookie value:

		        // Fetch some details to be passed to the remote web application
		        // e.g.: 
		        //
		        // string uid = [UserInfo.GetUID(user);]
	

		        string user = [HttpContext.Current.User.Identity.Name;]
		        bool adminRole = HttpContext.Current.User.IsInRole("admin");
	

		        // We create a concatenated cookie value containing all of the details
		        // that we would like to pass to the remote application.
	

		        // We also include a [DateTime] string, to help mitigate the risk of session replay
		        //
		        cookieval = user + "|" + uid + "|" + [adminRole.ToString()] + "|" + 
		                [DateTime.Now.ToBinary().ToString();]
	

		        // Considerations to help protect against replay:
		        //   
		        //  - Minimize the interval during which cookies are accepted from the remote site
		        //  - Consider implementing a sequence ID, in which used sessions are marked accordingly
		        //    in the database
		        //  - Leverage a secure session transport (SSL)
		        //  - Use restrictive cookie settings (Path, [HttpOnly,] Secure mode, restrict the domain
		        //          + Consider implementing a sub domain to be used by applications in which
		        //            cookie sharing is necessary (e.g. .ourapps.microsoft.com)
	


Encrypt, sign and set cookie value:

		        // Instantiate our DPAPI helper functions
		        [DPAPIUtil] dputil = new [DPAPIUtil();]
	

		        // Retrieve the symmetric encryption key we wish to use
		        byte[] enckey = [dputil.RetrieveSymmKey();]
	

		        // Create the Crypto provider
		        Rijndael rij = Rijndael.Create();
	

		        // Load the DPAPI protected key
		        rij.Key = enckey;
	

		        // By default we have a random IV from when we made the call to Rijndael.Create()
		        // We'll just prepend the IV to the encrypted value so the remote app can use our
		        // random IV. Alternatively it is possible to generate a new Random Initialization 
		        // Vector manually by calling:
		        //   rij.GenerateIV();
	

		        [MemoryStream] ms = new [MemoryStream();]
		        [CryptoStream] ecs = new [CryptoStream(ms,] [rij.CreateEncryptor(),] [CryptoStreamMode.Write);]
	

		        // Create the HMAC for our plaintext value
	

		        // Now we need to validate the HMAC to ensure the message wasn't altered
		        // We use a distinctly seperate HMAC key to ensure that if the encryption key
		        // is compromised message integrity may still be guaranteed and protects us against
		        // parameter tampering.
		        HMACSHA256 hmac = new HMACSHA256();
	

		        // Retrieve the HMAC key from the encrypted registry value using our DPAPI helper
		        hmac.Key = [dputil.RetrieveHMACKey();]
	

		        // Compute the HMAC on our plaintext cookie string
		        byte[] hmacValue = [hmac.ComputeHash(UnicodeEncoding.ASCII.GetBytes(cookieval));]
	

		        // Write the cookie contents to our cryptostream
		        [ecs.Write(UnicodeEncoding.ASCII.GetBytes(cookieval),] 0, cookieval.Length);
		        // Write the HMAC to our cryptostream
		        ecs.Write(hmacValue, 0, hmacValue.Length);
		        ecs.Close();
	

		        // Create a byte array to store our encrypted value plus IV
		        byte[] [encryptedValueWithIV] = new byte[rij.IV.Length + ms.ToArray().Length];
	

		        // Prepend the IV to our encrypted value. The initialization vector need not be kept
		        // secret, in fact randomizing this value for each encryption
		        Array.Copy(rij.IV, [encryptedValueWithIV,] rij.IV.Length);
		        [Array.Copy(ms.ToArray(),] 0, [encryptedValueWithIV,] rij.IV.Length, [ms.ToArray().Length);]
	

		        string encryptedCookie = [Convert.ToBase64String(encryptedValueWithIV);]
	

		        [HttpCookie] chocolateChip = new HttpCookie("CookieJar", encryptedCookie);
	

		        // Ensure that proper secure cookie modes are set:
		        //
		        // Make cookies unavailable to client side scripts
		        [chocolateChip.HttpOnly] = true;
		        // Set the cookie transport mechanism to SSL only
		        chocolateChip.Secure = true;
		        // Ideally choose a more restrictive domain under which cookies are set
		        chocolateChip.Domain = ".ourapp.microsoft.com";
		        // Place some restrictions on which web paths can access our cookies
		        chocolateChip.Path = "/CookieExample/";
	

		        // Finally set the cookie
		        Response.Cookies.Add(chocolateChip);
	

Problem Example

The following example demonstrates a poorly designed cookie sharing mechanism between two web applications where encryption and key signing are not used:

Set the cookie value:
		        cookieval = user + "|" + uid + "|" + [adminRole.ToString();]
	

		        [HttpCookie] chocolateChip = new HttpCookie("CookieJar", cookieval);
	

		        // Ensure that proper secure cookie modes are set:
		        //
		        // Make cookies unavailable to client side scripts
		        [chocolateChip.HttpOnly] = true;
		        // Set the cookie transport mechanism to SSL only
		        chocolateChip.Secure = true;
		        // Ideally choose a more restrictive domain under which cookies are set
		        chocolateChip.Domain = ".ourapp.microsoft.com";
		        // Place some restrictions on which web paths can access our cookies
		        chocolateChip.Path = "/CookieExample/";
	

		        // Finally set the cookie
		        Response.Cookies.Add(chocolateChip);
	

Issues found in this example:
* Sensitive information is exposed within the cookie, potentially containing data which should never be exposed to a user
* Cookies are vulnerable to tampering by the user (and intermediaries in the event of a man-in-the-middle scenario)
* Cookies are vulnerable to replay
* If cookies are altered in transit there is no way to detect such a situation
* Any web application in the .ourapp.microsoft.com domain can access the cookie.

Careful consideration should be given to other common coding mistakes not shown in the code above:
* Encryption using weak encryption keys (typically <128, but dependent on encryption algorithms)
* Use of unproven or homegrown encryption algorithms
* Use of improper encryption types (e.g. stream cipher instead of block cipher)
* Use of improper encryption modes (lack of chaining blocks)
* Leads to easier cookie tampering even with encryption (replace or reorder blocks)
* Leads to information disclosure of common values transmitted
* Failure to choose random initialization vector (IV)
* Leads to information disclosure of common values transmitted
* Failure to include HMAC
* Encrypted values may still be altered and go undetected
* HMAC keys and encryption keys are equal
* Compromise of one key leads to total system compromise
* Failure to include timestamp or session id
* Encrypted value may be replayed

Test Case

The following classes must be included in any project making use of the sample code provided above:

using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Security.AccessControl;
using System.Text;
using Microsoft.Win32;


Browsing to the web application containing the solution code will establish an encrypted and signed cookie value.

The following cookie value is set by the server with secure cookie options:

Set-Cookie: CookieJar=g2rzx1QZ94lAOyUxLGscUbQg1/yyG8Gy4NTCjaK5aupWwSXoAdyuNBer0sFZQiK9dx4kZnY0h8hd9xqEe6d8hMvGoJiGhlOh88cx35Jt9sQIQmjAgCmecj6VhHwKfbrZ; domain=.ourapp.microsoft.com; path=/CookieExample/; secure; HttpOnly

Observe cookie values which contain the following properties:
* Change with each encryption operation (due to random IV, and appended timestamp value)
* Base64 decoding the "CookieJar" value yields and encrypted array of bytes


Expected Result

HTTP Response header:
Set-Cookie: CookieJar=g2rzx1QZ94lAOyUxLGscUbQg1/yyG8Gy4NTCjaK5aupWwSXoAdyuNBer0sFZQiK9dx4kZnY0h8hd9xqEe6d8hMvGoJiGhlOh88cx35Jt9sQIQmjAgCmecj6VhHwKfbrZ; domain=.ourapp.microsoft.com; path=/CookieExample/; secure; HttpOnly


More Information

This implementation makes use of the DPAPI user key as opposed to the machine key. This means that the AES shared secret and HMAC signing key will only be accessible from programs that run within the same user context and will not be accessible by other applications running under different service accounts.

This adds additional protection against a rogue application (such as a virus or trojan) compromising connection string data but could pose problems where sharing between mutliple applications running under different accounts is required. We recommend this approach whenever other programs will not need to access the same key material for encryption operations.

Encryption of the message alone is not sufficient to protect against message tampering, as such we've included the use of an HMAC (or message integrity check, which relies on a shared secret key). The HMAC is generated by using a seperate shared secret and relies on a strong one-way hashing algorithm to generate a unique hash for the given message.

Even with cookie encryption and signing, a user who intercepts these cookies may replay them to the server unless there is a value contained within the message designating the sequence number of the message.

The symmetric shared key and HMAC signing key must be securely stored in order to maintain the confidentiality of data encrypted using these keys. In this example, we make use of the DPAPI in order to transparently stored the shared key pair encrypted with in the registry.

The details of AES and HMAC message signing are beyond the scope of this article. However the following topics are discussed in the articles in Additional Resources:
* Encrypt a string via a block cipher (using AES)
* Generate a Message Authentication Code (HMAC)



Additional Resources

* HTTP Request Cookies Class: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemwebhttprequestclasscookiestopic.asp
* HTTP Cookies: http://msdn.microsoft.com/library/en-us/wininet/wininet/http_cookies.asp
* Cookie Members: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemnetcookiememberstopic.asp
* HMAC definition: http://en.wikipedia.org/wiki/HMAC
* HMAC class: http://msdn2.microsoft.com/en-US/library/system.security.cryptography.hmac.aspx
* Protecting keys using the DPAPI: http://msdn.microsoft.com/library/en-us/dnpag2/html/paght000005.asp
* Security Guidelines (.NET Cryptography): http://msdn.microsoft.com/library/en-us/dnpag2/html/PAGGuidelines0003.asp?frame=true#pagguidelines0003_cryptography

Attributes

* Applies To: .NET Framework 2.0, C#
* Category: Session Management
* Author: George Gal




Return to HomePage
Microsoft Communities