Cryptographic Improvements in ASP.NET 4.5, pt. 1
I am Levi Broderick, a developer on the ASP.NET team at Microsoft. In this series, I want to introduce some of the improvements we have made to the cryptographic core in ASP.NET 4.5. Most of these improvements were introduced during beta and spent several months baking. When you create a new project using the 4.5 templates baked into Visual Studio 2012, those projects will take advantage of these improvements automatically. The intent of this series is both to explain why the ASP.NET team made these investments and to educate developers as to how they can take maximum advantage of this system.
This series will be divided into three posts:
- Background regarding the use of cryptography in ASP.NET 4 (today’s post).
- Changes that were introduced in ASP.NET 4.5.
- Usage notes and miscellaneous Q&A.
Ever since ASP.NET’s inception over a decade ago, the product has consumed cryptography in some form. We have a variety of use cases: ViewState, ScriptResource.axd and WebResource.axd URLs, FormsAuthentication tickets, membership passwords, and more. And for a while, we just assumed that the types in the System.Security.Cryptography namespace solved all our problems automatically, ignorant of the fact that the callers have the ultimate responsibility to call the APIs correctly. This led to MS10-070, whereby attackers exploited the fact that ASP.NET misused these cryptographic primitives and were able to read sensitive files from the web application directory.
We quickly released a patch for that issue, but at the same time we realized that we needed to perform a more thorough investigation of cryptographic uses inside ASP.NET. Through a joint effort between members of the ASP.NET security team, the .NET Framework security team, and Microsoft’s crypto board, we identified several areas for improvement and set to work drafting changes.
A brief digression: auto-generated machine keys
Whenever any discussion of cryptography in ASP.NET comes up, the topic of conversation eventually comes around to the <machineKey> element. And the confusion is understandable since the term is overloaded. There are four attributes in particular which are most immediately interesting.
|decryption||An algorithm which performs encryption and decryption using a symmetric key.|
|decryptionKey||A hex string specifying the key used by instances of the decryption algorithm.|
|validation||An algorithm which generates a message authentication code over some payload.|
|validationKey||A hex string specifying the key used by instances of the validation algorithm.|
The format of decryptionKey and validationKey is as follows: key-format = (hex-string | ("AutoGenerate" [",IsolateApps"] [",IsolateByAppId"]))
Normally these keys are expected to be represented by hex strings, but developers can also specify that ASP.NET use auto-generated keys instead of explicitly-specified keys. If an auto-generated key is used, the runtime will automatically populate the registry key HKCU\Software\Microsoft\ASP.NET\4.0.30319.0\AutoGenKeyV4 with a random number generated by a cryptographically-secure RNG. The registry key holds enough random bits for both an encryption key and a validation key to exist side-by-side without overlapping, and the value is itself protected using DPAPI.
There is an important consequence of the above: the auto-generated machine key is unique per user (where the user is usually the Windows identity of the worker process) on a given machine. If two web applications are deployed on a machine, and if those applications’ process identities are equivalent, then the auto-generated keys will be the same for both applications.
Throughout this series, the term “machine key” refers to the tuple of decryptionKey and validationKey. When I refer to the machine key being transformed, envision that the transform is being applied to the decryption and validation keys independently. It is also possible to use an auto-generated decryptionKey with an explicit validationKey and vice versa, but this configuration is discouraged.
ASP.NET provides two optional modifiers that can further alter the auto-generated machine key before it is consumed by the application. (The particular transformation mechanism is described later in this post.) The currently supported modifiers are:
- IsolateApps – The runtime uses the value of HttpRuntime.AppDomainAppVirtualPath to transform the auto-generated key. If multiple applications are hosted on the same port in IIS, the virtual path is sufficient to differentiate them.
- IsolateByAppId – The runtime uses the value of HttpRuntime.AppDomainAppId to transform the auto-generated key. If two distinct applications share a virtual path (perhaps because those applications are running on different ports), this flag can be used to further distinguish them from one another. The IsolateByAppId flag is understood only by the ASP.NET 4.5, but it can be used regardless of the compatibilityMode setting (which will be introduced in tomorrow’s post).
If no explicit hex key is specified in an application’s configuration file, then the runtime assumes a default value of AutoGenerate,IsolateApps. Thus by default the user’s auto-generated machine key is transformed with the application’s virtual path, and this transformed value is used as the cryptographic key material.
The world in ASP.NET 4
Transforming the auto-generated machine key
One notable change to ASP.NET 4’s cryptographic pipeline is that we added support for the SHA-2 family of algorithms. This was possible partly due to the fact that Windows XP / Server 2003 were the minimum system requirements for .NET 4, and the latest service pack for both OSes at the time brought native support for SHA-2. We also added a configuration option for specifying the particular algorithms used, so any developer is able to swap in their own SymmetricAlgorithm or KeyedHashAlgorithm-derived types.
When the runtime is asked to use auto-generated machine keys in ASP.NET 4, it selects AES with a 192-bit key (this is a holdover from when ASP.NET used Triple DES, which takes a 192-bit key) and HMACSHA256 with a 256-bit key. Consider only the encryption key for now. The auto-generated machine key as retrieved from the registry will provide the full 192 bits of entropy. Assume that those bytes are:
ee 1c df 76 16 ed 18 37 70 05 30 a8 17 d0 e6 69 97 65 21 de 00 3b 92 70
Remember: the auto-generated key as stored in the registry contains both the encryption and the validation keys. The encryption and validation keys are extracted individually from this registry entry, and the transformation is applied to each independently.
Furthermore, recall that if IsolateApps is specified, this key is further transformed before being consumed by the application. The particular manner in which this occurs is that the runtime hashes the application’s virtual path into a 32-bit integer, and these 32 bits replace the first 32 bits of the value that we got from the registry. Thus if the application’s virtual path is “/myapp”, and if that string hashes to 0x179AB900, then IsolateApps will transform the key read from the registry into:
17 9a b9 00 16 ed 18 37 70 05 30 a8 17 d0 e6 69 97 65 21 de 00 3b 92 70
The immediate consequence of this is that the 192-bit key used for encryption contains only 160 bits of entropy. (If IsolateByAppId is also specified, then the next 32 bits will be likewise replaced by the hash of the application’s AppDomainAppID, and the total entropy is reduced to 128 bits.) It is important to note that neither the application’s virtual path nor its application ID is secret knowledge, and in fact the former is trivial to guess since it is often in the URL itself. So if an application is deployed to the virtual path “/” and is using the default behavior of AutoGenerate,IsolateApps, it can be assumed that the first 32 bits of encryption key material are 4e ba 98 b2, which is the hash of the string “/”.
Transformation of the validation key works in a similar fashion. The default auto-generated key is a 256-bit key for use with HMACSHA256. IsolateApps reduces the entropy to 224 bits, and IsolateApps and IsolateByAppId together will reduce the entropy to 192 bits.
Use of key material
The particular design of <machineKey> requires that there be only a single set of encryption and validation keys at any given time. There are two implications to this design. The first is that this presents a hardship to organizations which require that cryptographic key material be refreshed on a regular basis. The standard way of doing this is to update the keys in Web.config, then redeploy the affected applications. However, all existing encrypted or MACed data will then be rendered invalid since the framework will no longer be able to interpret existing ciphertext payloads.
The second implication has a greater impact on application security but is more subtle to most observers. Things should come more into focus with a bit of exposition.
The API FormsAuthentication.Encrypt takes as a parameter a FormsAuthenticationTicket instance, and it returns a string corresponding to the protected version of that ticket. More specifically, the API serializes the FormsAuthenticationTicket instance into a binary form (the plaintext), and this binary form is run through encryption and MACing processes to produce the ciphertext. Typical usage is as follows:
var ticket = new FormsAuthenticationTicket("username", false, 30); string encrypted = FormsAuthentication.Encrypt(ticket);
(A similar code path is invoked by FormsAuthentication.SetAuthCookie and other related methods.)
In earlier versions of ASP.NET, the ticket serialization routine automatically prepended 64 bits of randomness before outputting fields like the username, creation and expiration dates, etc. Assuming a good RNG, there is a 1 in 256 chance of the first byte being any particular value, such as 0x54. This would normally seem harmless, but…
The ScriptResourceHandler (ScriptResource.axd) type provides several services for AJAX-enabled ASP.NET applications. The API is called via ScriptResource.axd?d=xyz, where xyz is ciphertext. ScriptResourceHandler will extract the plaintext and perform some action depending on the value of the first plaintext byte. If this first byte is 0x54, the plaintext payload is dumped to the response. (This behavior is intended to support AJAX navigation on browsers which do not include native support for the feature.)
Putting these two details together, one reasons that there is a 1 in 256 chance that an encrypted FormsAuthentication ticket given to a client can be echoed back to ScriptResource.axd for decryption. This highlights the second implication mentioned above: since there is a single set of cryptographic keys for the application, all components necessarily share the same set of keys. All cryptographic consumers within an application need to be aware of each other and differentiate their payloads; otherwise, attackers can start playing the individual consumers off one another. A weakness in a single component can quickly turn into a weakness in an entirely different part of the system.
The project FormsAuthScriptResource in the sample solution demonstrates the above problem with forms authentication and ScriptResource.axd. The application mimics logging out and back in several times in succession until ScriptResource.axd accepts the provided forms authentication ticket as valid. Keep in mind that since we fixed this particular bug as part of MS11-100, I have modified this particular project’s Web.config such that the application exhibits the old (pre-fix) behavior. This was done strictly for educational purposes, and production applications should never disable any of our security fixes.
(It should be noted that the root cause of CVE-2011-3416 was a forms authentication ticket serialization flaw that was privately disclosed to us by a third party. The “payload collision” flaw mentioned above was an internal find by our security team. Since fixing CVE-2011-3416 required us to change the forms authentication ticket payload format anyway, we just piggybacked the payload collision fix on top of it, and the whole package went out as part of the MS11-100 release.)
The MachineKey public APIs
Finally, I want to discuss the MachineKey.Encode and Decode APIs. These APIs were added in ASP.NET 4 due to high customer demand for some form of programmatic access to the crypto pipeline. A common use case is that the application needs to round-trip a piece of data via an untrusted client and doesn’t want the client to decipher or tamper with the data. The easiest way to write such code in 4.0 is:
string ciphertext = ...; // provided by client byte decrypted = MachineKey.Decode(ciphertext, MachineKeyProtection.All);
As described above, since the same cryptographic keys are used throughout the ASP.NET pipeline, it turns out that the Decode method can also be used to decrypt payloads like forms authentication tickets. Consumers of the Decode method can try to defend against clients passing these payloads through, but it requires developers to be cognizant of the fact that the Decode method can even be abused in this manner.
Even more dangerous is the way in which the Encode method is often called:
byte plaintext = ...; // provided by client string ciphertext = MachineKey.Encode(plaintext, MachineKeyProtection.All);
Now consider what happens if a client provides this payload:
01 02 83 c7 3d c6 96 53 cf 08 fe 00 80 30 05 6d f3 d1 08 00 05 61 00 64 00 6d 00 69 00 6e 00 00 01 2f 00 ff
This payload happens to correspond to the current serialized forms authentication ticket format (post-MS11-100). The ticket has a username of “admin” and an expiration date of January 1, 2015. If this payload is passed to MachineKey.Encode, and if the resulting ciphertext is then returned to the client, then the client has successfully managed to forge a forms authentication ticket.
It should be apparent that these APIs are a double-edged sword. By providing access to the cryptographic pipeline, we are providing developers with a great deal of power, but we are also trusting developers to use the APIs correctly. And therein lies the problem: correct usage requires intimate knowledge of ASP.NET internal payload formats (not just existing formats, but also any format we might add in the future!), and this is simply an onerous and unrealistic expectation. It’s a pit of failure, delicately lined with crocodiles and pointy spikes.
In tomorrow’s post, I’ll discuss pipeline changes in ASP.NET 4.5, including new configuration switches and APIs that lend themselves to the pit of success.