Return to HomePage



Decrypting and Validating the Signature of a Cookie Set 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 decrypt and validate an encrypted application cookie set by a trusted application. This code example demonstrates how to verify the integrity and freshenss (e.g. timestamp) of the message contained within the encrypted blob. This code sample may be used when there exists no common server-side session state.


Objectives

* Provide controls to mitigate session replay
* 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
* Provide controls which validate message integrity and detect message tampering
* Ensure the secure storage of a shared secret encryption key and HMAC signing key through use of the Data Protection API (DPAPI)


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;]
		    }
	

}


Decrypting and validating the encrypted & signed key in our cookie

Retrieve the cookie values:
		        bool isValidHMAC = false;
		        bool isValidTS = false;
	

		        // Read our encrypted cookie value
		        byte[] encvalWithIV = Convert.FromBase64String(Request.Cookies["CookieJar"].Value);
	

		        byte[] iv = new byte[16];  // We're expecting a 16 byte IV
		        byte[] encval = new byte[encvalWithIV.Length - 16]; // Byte array for our message without IV
	

		        // Parse the IV from the first 16 bytes of the cookie
		        Array.Copy(encvalWithIV, iv, iv.Length);
		        Array.Copy(encvalWithIV, 16, encval, 0, encval.Length);
	

		        // We'll retrieve our symmetric encryption key from the registry
		        [DPAPIUtil] dputil = new [DPAPIUtil();]
		        byte[] enckey = [dputil.RetrieveSymmKey();]
	

		        // Preparation to decrypt our cookie value:
		        //     
		        // Create the Crypto provider
		        Rijndael rij = Rijndael.Create();
	

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

		        [MemoryStream] ms = new [MemoryStream();]
	

		        // Instantiate our decryptor stream to which we write our encrypted value, our
		        // memory stream will contain the decrypted value to be used by our application
		        [CryptoStream] dcs = new [CryptoStream(ms,] [rij.CreateDecryptor(),] [CryptoStreamMode.Write);]
	

		        // Write the decrypted value to the cryptostream
		        dcs.Write(encval, 0, encval.Length);
		        dcs.Close();
	

		        string decryptedCookie = [UnicodeEncoding.ASCII.GetString(ms.ToArray());]
	

		        // 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();]
	

		        // We store the HMAC in the last 32 bytes of the message
		        byte[] hmacValue = new byte[32];
		        [Array.Copy(ms.ToArray(),] [ms.ToArray().Length] - 32, hmacValue, 0, hmacValue.Length);
	

		        // To validate the HMAC we'll recompute an HMAC using the HMAC key and compare this with
		        // the HMAC sent to us by the remote application
		        string msg = decryptedCookie.Substring(0, decryptedCookie.Length - 32);
	

		        byte[] [hmacValueNew] = [hmac.ComputeHash(UnicodeEncoding.ASCII.GetBytes(msg));]
	

		        if [(Convert.ToBase64String(hmacValue)] == [Convert.ToBase64String(hmacValueNew))]
		        {
		            isValidHMAC = true;
		        }
		        else
		        {
		            // Invalidate our session and throw an error
		            Session.Abandon();
	

		            throw new Exception("Invalid HMAC Detected");
		        }
	

		        [lblHMACBool.Text] = [isValidHMAC.ToString();]
	

		        // Lastly validate the timestamp to ensure it is no more than 5 minutes old
		        //
		        // The timestamp is the last value stored in our message
		        string ts = msg.Substring(msg.LastIndexOf("|") + 1);
		        [DateTime] cookieTs = [DateTime.FromBinary(long.Parse(ts));]
		        [DateTime] curTs = [DateTime.Now;]
	

		        // Determine how old the cookie is that we're trying to validate. 
	

		        // In our example below we simply check to make sure the cookie isn't older than
		        // 5 minutes; This technique doesn't elliminate session replay but does help to minimize
		        // the window of opportunity in which an attacker may replay a session.
		        //
		        // In an ideal scenario it may be advantageous to use a sequence number which we 
		        // store in the database each time a cookie is used, so that it may not be used twice.
		        [TimeSpan] tspan = new [TimeSpan(curTs.Ticks] - cookieTs.Ticks);
	

		        // If the cookie is older the 5 minutes we'll require the user to obtain a new cookie
		        if (tspan.Minutes > 5)
		        {
		            // Redirect to our source site
		            Response.Redirect("/CookieExample/SetCookie.aspx");
		        }
		        else
		        {
		            isValidTS = true;
		            //Parse the cookie and consume its values after performing data validation
		        }
		        [lblTSBool.Text] = [isValidTS.ToString();]
	

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);
	

Retrieve and use the cookie value:
		        cookieval = Request.Cookies["CookieJar"].Value;
	

// Parse and consume the cookie value attributes
...

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


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.


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. 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