Security Briefs

CardSpace, SqlMembershipProvider, and More

Keith Brown

Code download available at:SecurityBriefs2006_10.exe(151 KB)

In this month's column, I'd like to take time to answer a few questions that I frequently get from readers. I'll cover information cards, the SqlMembershipProvider in ASP.NET 2.0, and access control GUIs.

Q How can I discover the long-term key for a security token from a self-issued information card?

Q How can I discover the long-term key for a security token from a self-issued information card?

A In a recent column that covered InfoCard (which has since been renamed Windows® CardSpace), I talked about using a self-issued information card in place of a user name and password. At about the same time I wrapped that column, the February community technology preview (CTP) shipped. Unfortunately, that was not a good release for CardSpace and most CardSpace samples stopped working under that release. However, in late May, Microsoft released a new version that brought CardSpace back to life.

A In a recent column that covered InfoCard (which has since been renamed Windows® CardSpace), I talked about using a self-issued information card in place of a user name and password. At about the same time I wrapped that column, the February community technology preview (CTP) shipped. Unfortunately, that was not a good release for CardSpace and most CardSpace samples stopped working under that release. However, in late May, Microsoft released a new version that brought CardSpace back to life.

I've got a new and improved Notepad Web service example that demonstrates how to discover the user's long-term key. Before I show you how to do this, let me explain why this is important. Say you write a service that accepts self-issued security tokens. When Alice contacts your service, the client-side CardSpace plumbing creates a security token with the claims you've requested. These claims can be an e-mail address, a phone number, or any other personal information that can be stored in a self-issued card. Of course, this could also be an identifier that allows the user to remain anonymous. This is the personal private identifier (PPID) I've talked about in earlier columns. E-mail addresses are common, but PPIDs are handy when user anonymity is important.

Imagine that you use an e-mail address to identify the user. When Alice first visits your site and sends you a self-issued security token, you record her e-mail address as a new account. When she visits a second time, you can look up her user account data using the e-mail address you receive in the security token. But how do you know the person on the other end is really Alice and not a different user trying to impersonate her? There's no longer a password, so what secret information is being used to authenticate the user?

Remember, the security token that is being sent by the user will be signed with a private key that only the user's self-issued security token service knows. The corresponding public key will be included in the signature so that Windows Communication Foundation (or whatever type of plumbing your relying party uses) can verify the signature. This is your cryptographic assurance that the client does, in fact, know her private key. However, Windows Communication Foundation doesn't know what the correct public key is for any given user. If an attacker wants to impersonate alice@fabrikam.com, all he needs to do is generate a new public/private key pair, sign his token with the private key, and send the corresponding public key in the signature. It's easy to forget this little detail, but while Windows Communication Foundation is happy to do the heavy lifting by verifying the signature against the submitted public key, it's still your job to make sure that the submitted public key is the same public key the user initially used.

So how do you discover this public key? It's easy using AuthorizationContext in Windows Communication Foundation. Enumerating the claims sent by your user gives you the e-mail address you requested as well as the sender's RSA public key.

Before any privacy advocates get upset with me, let me clarify what this all really means. If there were only one public key used for all interactions, it would be impossible to maintain anonymity across relying parties. If those parties had no scruples, they could use your public key as an identifier and easily correlate all the claims you gave each of them to create a shared dossier of information about you. This is clearly a scenario that should be avoided!

To understand what's really going on, recall how the PPID is constructed. It's essentially a hash of various bits of information that comes from the relationship between a particular information card and a particular relying party. You can use the same information card with 10 different relying parties and each relying party will see a different PPID since the information they provide (such as their public key) will differ. Well, the self-issuing security token service generates RSA key pairs in a similar way. Each of those 10 relying parties will see a different public key, even if you use the same information card. This helps keep the user in control of her identity.

Figure 1 shows the code you can use to retrieve this public key. Note that the URIs for self-issued information card claims have changed slightly since my last column.

Figure 1 Retrieve the Public Key

object getCallerPublicKey() { const string CLAIMTYPE_RSA = https://schemas.microsoft.com/ws/ "2005/05/identity/claims/Rsa"; AuthorizationContext ctx = ServiceSecurityContext.Current.AuthorizationContext; foreach (ClaimSet cs in ctx.ClaimSets) { foreach (Claim c in cs) { if (Rights.Identity == c.Right && CLAIMTYPE_RSA == c.ClaimType) { return c.Resource; } } } throw new Exception("Expected an RSA claim"); }

An RSA public key isn't just a string of bytes. It actually comes in two parts: a modulus and an exponent. When you read the public key claim from AuthorizationContext, you actually get an object that extends the RSA abstract class, which itself extends AsymmetricAlgorithm in the System.Security.Cryptography namespace. To extract the public key parameters, you need to downcast and call RSA.ExportParameters to get access to the modulus and exponent. You hash them together to produce a unique identifier for the key that you can store in your database. The method shown in Figure 2 does just that. If you don't mind storing binary data, you can skip the Base64 encoding step; this will produce a 32-byte identifier for the key.

Figure 2 Produce a Unique Identifier

string hashPublicKey(object rsaKey) { RSAParameters keyParams = ((RSA)rsaKey).ExportParameters(false); SHA256Managed hashAlg = new SHA256Managed(); hashAlg.TransformBlock(keyParams.Exponent, 0, keyParams.Exponent.Length, null, 0); byte[] hash = hashAlg.TransformFinalBlock(keyParams.Modulus, 0, keyParams.Modulus.Length); return Convert.ToBase64String(hash); }

The Notepad sample I discussed in my earlier column used a PPID to identify users while allowing them to remain anonymous. And it kept a little piece of state for each user—a note—which he could access by calling GetNote or SetNote and presenting a self-issued security token to identify himself. My updated sample differs in that it doesn't index the notes based only on PPID. Instead, it combines the PPID and the hash of the public key to form a user identifier. This way if an attacker submits Alice's PPID along with his own public key, he won't be able to see Alice's note. Figure 3 shows the new code for GetNote and SetNote, showing how to pair up the PPID with the public key hash to index into the dictionary where I keep state for each user.

Figure 3 Pairing the PPID with the Public Key Hash

public class Notepad : INotepad { static Dictionary<string, string> map = new Dictionary<string, string>(); public string GetNote() { string userId = getUserIdentifier(); lock (map) { if (map.ContainsKey(userId)) return mapUserIdToNotes[userId]; else return null; // new user } } public void SetNote(string data) { string userId = getUserIdentifier(); lock (map) { map[userId] = data; } } string getUserIdentifier() { string ppid = getCallerPPID(); string publicKeyHash = getCallerPublicKeyHash(); return ppid + publicKeyHash; } // ... }

In general, you should consider storing the user's public key as part of any user tracking mechanism you rely upon. A column in a Users database table, for instance, would serve this purpose well. When you enumerate the claims in the self-issued token, grab the RSA claim as well and verify that it's the same key the user presented in the past. Be sure to download my latest sample code from the MSDN®Magazine Web site, which works with Beta 2 of Windows CardSpace.

A The SqlMembershipProvider ships with three password formats: Clear, Encrypted, and Hashed. These values are found in the MembershipPasswordFormat enumeration, and you can select which one to use via configuration. If you already require your users to supply a password in order to log in, your system probably has assets that require protection—otherwise you wouldn't inconvenience your users with a logon prompt.

A The SqlMembershipProvider ships with three password formats: Clear, Encrypted, and Hashed. These values are found in the MembershipPasswordFormat enumeration, and you can select which one to use via configuration. If you already require your users to supply a password in order to log in, your system probably has assets that require protection—otherwise you wouldn't inconvenience your users with a logon prompt.

When you require users to submit passwords, you owe them a certain amount of due diligence. Exposing passwords in cleartext so that anyone with SQL SELECT privileges to the membership table can casually browse them is simply negligent. So that rules out Clear in production scenarios.

What about Encrypted? Well, one consideration is that if you encrypt a user's password, you can always decrypt it and communicate it to her if she forgets it. While it might be tempting to simply e-mail the password to the user, e-mail messages are normally sent in the clear and are not at all a secure way to transmit secret information like passwords. Unless the password is basically a nuisance fee for using your Web site, you should probably use an SSL protected Web page. But as you'll see soon enough, the main problem with encrypted passwords is that you need a secret to use them.

The secret key that SqlMembershipProvider uses to encrypt passwords is the one specified in the <machineKey> element in the configuration file. If you don't specify this configuration element, it is set to the equivalent of AutoGenerate, IsolateApps. With the AutoGenerate option, ASP.NET uses a randomly generated master key that is stored on the machine. Meanwhile, the IsolateApps option tells ASP.NET to derive individual keys for each application by modifying the master key based on the hash of the application's virtual directory. This doesn't work very well for encrypting passwords because there's no easy way to back up this data. If you are developing on one machine and testing on another, you need to create new test user accounts for each system. And if you lose the encryption key in a production system, you lose the ability to encrypt or decrypt passwords and thus you can no longer authenticate your existing users. So the first problem you'll likely encounter is the exception that the ASP.NET team put in to warn you not to use auto-generated keys if you're going to encrypt passwords.

To solve this problem, you need to generate a strong, random key and add the <machineKey> element to your configuration file. (Don't let the name of this element fool you—you can put this in your web.config file if you want your application to have its own individual key). If you need a tool to help you generate the key, surf to www.pluralsight.com/tools.aspx and download the ASP.NET Administration Console. This tool will help you generate the strongest possible <machineKey> that ASP.NET 2.0 supports, and since the tool comes with source code, you can verify that it doesn't e-mail a copy of the key to me. All jokes aside, this is a serious consideration when using tools of this nature. In addition, you should avoid the IsolateApps feature so that your key doesn't depend on the name of your virtual directory, since the name could very well change over time.

You don't want that key sitting around visible for any sysadmin to see through casual browsing. You can use aspnet_regiis to encrypt the <machineKey> section once you've made a backup copy of the key. To learn how this is done, see the patterns & practices article "How To: Encrypt Configuration Sections in ASP.NET 2.0 using DPAPI". It's really quite easy.

Now here's the really bad news. Because all user accounts are encrypted using <machineKey>, if you change that key, you'll need to rekey your user database, and there's no easy way to do that with the providers as they stand today. So once you do this, your <machineKey> becomes very difficult to change. And a key that is hard to change is a liability. What happens when your system administrator quits and joins the competition? Things could get uncomfortable. And many other security features, such as login cookie protection and view state protection, all depend on that same key that you can no longer easily change. In order to change the <machineKey> you'll need to write code to rekey your membership user database and there's no built-in mechanism to make this an easy thing to do.

So my recommendation is to use the Hashed password format. When you add a new user account with SqlMembershipProvider, it generates a salted hash based on the password and stores the salt and the hash in the user's record as a password verifier. (I described this technique in detail in the August 2003 Security Briefs column at msdn.microsoft.com/msdnmag/issues/03/08/SecurityBriefs.) When a user tries to log in, the provider looks up the user's account based on the name she has provided, reads the salt from the record, and uses that to compute a hash of the password supplied by the user. If the hash matches the stored verifier, the user is authenticated and allowed to move on.

Some functionality of SqlMembershipProvider isn't available if you choose Hashed passwords. For example, if you try to retrieve the user's password, you'll be treated with an exception since there's no direct way to reverse the hash. If a user forgets her password, you can use the ResetPassword method to have the membership system generate a new random password, which you must communicate to the user. She can then change her password using the ChangePassword control. Of course, now you need to worry about having a secondary means of authenticating the user. Otherwise, you'll have bad guys resetting passwords for legitimate users in order to take over their accounts. The question and answer system built into the Membership feature can help with this.

By the way, just because you can't directly reverse the hash doesn't mean the passwords cannot be retrieved. If an attacker gets his hands on your user records in the membership table, he can mount an offline dictionary or brute force attack, guessing passwords and computing their hashes until he finds one that matches. This attack will quickly break weak passwords. A strong password policy is the best way to slow down this sort of attack. If this is a concern, use the attributes on the membership provider in your config file to require strong passwords. By default, the SqlMembershipProvider requires a minimum of 7 characters for a password, and one of those characters must be a non-alphanumeric symbol. You can go further if you like, specifying a regular expression that must be satisfied when a new user registers. You can even write code to validate new passwords by hooking up to the provider's ValidatingPassword event. If you end up writing your own password validation code, you can do things like checking for dictionary words in the password and so on. Figure 4 provides an example configuration that uses the Hashed password format, requiring an 8-character password with at least two non-alphanumeric characters.

Figure 4 Hashed Password Format in Action

<connectionStrings> <clear/> <add name="asp" connectionString="integrated securiyt=sspi;database=myaspnetdb" providerName="System.Data.SqlClient" /> </connectionStrings> <system.web> <!-- other entries omitted for brevity --> <membership defaultProvider="MyProvider"> <providers> <clear/> <add name="MyProvider" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" connectionStringName="asp" applicationName="MyApplication" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="true" requiresUniqueEmail="false" passwordFormat="Hashed" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="8" minRequiredNonalphanumericCharacters="2" passwordAttemptWindow="10" passwordStrengthRegularExpression="" /> </providers> </membership> </system.web>

Q Should I use the AclUIAdapter to edit the access control list on a file?

Q Should I use the AclUIAdapter to edit the access control list on a file?

A A reader recently asked how he should go about building a dialog box in managed code that is similar to the one in Windows Explorer that allows a user to edit the access control list (ACL) for a file. My first inclination was to suggest using the AclUIAdapter, which I presented in my March 2005 column.

A A reader recently asked how he should go about building a dialog box in managed code that is similar to the one in Windows Explorer that allows a user to edit the access control list (ACL) for a file. My first inclination was to suggest using the AclUIAdapter, which I presented in my March 2005 column.

But there’s a much easier solution. Let Windows Explorer handle the heavy lifting! There’s a function in Win32 called SHObjectProperties, which will display the Windows Explorer property sheet for files and printers. Here’s the Win32® definition:

BOOL SHObjectProperties( HWND hwndOwner, DWORD dwType, LPCWSTR szObject, LPCWSTR szPage );

The first argument is the owner window handle for the property sheet, which can be NULL if you don’t have a window handy. The second indicates the type of object for which Windows Explorer should display a property sheet. These types are defined in shlobj.h:

#define SHOP_PRINTERNAME 0x00000001 #define SHOP_FILEPATH 0x00000002 #define SHOP_VOLUMEGUID 0x00000004

Given this, it’s quite easy to whip up some P/Invoke stubs and call this function to display a property sheet. Figure 5 shows the code for FSP.EXE, short for File System Properties. Figure 6 shows the property sheet that pops up when it is run from the command line:

fsp security c:\autoexec.bat

This program could easily be modified to show a printer or volume property page as well.

Figure 5 Using SHObject Properties

using System; using System.Runtime.InteropServices; // fsp == file system properties // usage: fsp propertyPageName filename // fsp security c:\autoexec.bat class DisplayFileSystemPropertySheet { static void Main(string[] args) { if (2 != args.Length) { printUsage(); return; } string page = args[0]; string path = args[1]; if (!SHObjectProperties(IntPtr.Zero, ShellObjectType.FilePath, path, page)) { Marshal.ThrowExceptionForHR( Marshal.GetHRForLastWin32Error()); } Console.WriteLine("Press any key to continue..."); Console.ReadKey(); } static void printUsage() { Console.WriteLine("Usage: fsp propertyPageName filename"); Console.WriteLine("Example: fsp Security c:\\autoexec.bat"); } enum ShellObjectType : int { PrinterName = 1, FilePath = 2, VolumeGuid = 4 } [DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] static extern bool SHObjectProperties(IntPtr hwndParent, ShellObjectType type, string name, string page); }

Figure 6 Properties Displayed

Figure 6** Properties Displayed **

Send your questions and comments for Keith to  briefs@microsoft.com.

Keith Brown is a cofounder of Pluralsight, a premier Microsoft .NET training provider. Keith is the author of Pluralsight's Applied .NET Security course, as well as several books, including The .NET Developer's Guide to Windows Security, which is available both in print and on the Web.