The new security components in .NET 2.0 can help you greatly reduce the amount of code you need to write in order to make your applications secure. Security is difficult to get right, and it is a good strategy to leverage the code provided by Microsoft and other security vendors. To that end, .NET 2.0 provides numerous additional types that encapsulate functionality already provided in the base Windows OS., as well a new functionality only available in .NET 2.0. The improvements affect public key cryptography, Windows security, remoting, ASP.NET and Code Access Security. Even if you plan to stick with .NET 1.1 for a while and implement your own security classes, you might want to take inspiration from.NET 2.0 beta.
Certificates
and PKCS support.NET 1.1 already provides support for public key cryptography. The various types that derive from AsymmetricAlgorithm implement different standards for signing data using your private key, as well as encrypting data using the recipient’s public key. There is a caveat, however, because it uses keys directly – not certificates. Certificates represent a binding between a public key and the name of its owner and provide an efficient, convenient way to determine the real sender or recipient of a message.
.NET 2.0 introduces two new namespaces under System.Security.Cryptography: X509Certificates for certificates and Pkcs for Public Key Cryptography Standard.
While it is possible to store certificates in files, it is more convenient and more manageable to have them in a certificate store. Put simply, a certificate store is a database containing certificates. With the new X509Store class, you can open a store and query its certificates using several criteria including subject name and thumbprint. The new X509CertificateEx class is much richer and provides support for checking the certificate revocation list.
The following snippet finds a certificate and prints its status on the console:
X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
X509CertificateEx certificate =
store.Certificates.Find(X509FindType.FindBySerialNumber, serialNumber, false)[0];
X509Chain chain = new X509Chain();
...
chain.Build(certificate);
foreach (X509ChainElement e in chain.ChainElements)
{
foreach (X509ChainStatus s in e.ChainElementStatus)
{
Debug.WriteLine(s.Status);
Debug.WriteLine(s.StatusInformation);
}
Debug.WriteLine(e.Information);
}
In the Pkcs namespace, the new EnvelopedCms and SignedCms classes define ways to create encrypted or signed messages that contain a reference to the certificate used. As a result, processing this message is much easier because you don’t have to locate the key yourself.
For example, to encrypt a message, you simply specify the content to protect and the certificate to use.
ContentInfo contentInfo = new ContentInfo(stuffToEncrypt); EnvelopedCms envelopedMessage = new EnvelopedCms(contentInfo); CmsRecipient recipient = new CmsRecipient(SubjectIdentifierType.IssuerAndSerialNumber, recipientCertificate); envelopedMessage.Encrypt(recipient); byte[] encryptedBytes = envelopedMessage.Encode();
Decrypting the message is effortless because you don’t have to specify a key; the framework finds it automatically in the appropriate store based on the embedded serial number.
envelopedMessage = new EnvelopedCms();
envelopedMessage.Decode(encryptedBytes);
byte[] decryptedBytes = envelopedMessage.ContentInfo.Content;
foreach (RecipientInfo r in envelopedMessage.RecipientInfos)
{
Debug.WriteLine("The message was sent for " +
r.RecipientIdentifier.Value + (r.RecipientIdentifier.Type));
}
In addition, the Xml cryptography namespace has been improved to the level provided by the web services enhancements.
.NET 1.1 already provides classes for authentication (WindowsIdentity), authorization (WindowsPrincipal) and supports impersonation. .NET 2.0 enhances this with support for accounts, security identifiers, object level security, data protection API, and secure communication.
There are two methods to identify accounts: by name or by security identifier. Because they are readable by humans, account names (class NTAccount) are useful for user interfaces. Security identifiers (class SecurityIdentifier) encapsulate the binary representation used internally by Windows.
Among other things, it is possible to get the security identifier of a windows identity and to translate between account and security identifiers using the Translate method.
SecurityIdentifier sid = WindowsIdentity.GetCurrent().User; // Get the security identifier of the process token NTAccount account = (NTAccount)sid.Translate(typeof(NTAccount)); // Get the corresponding account.
Security identifiers are also used in object centric security to control access to different types of objects. The file system and the registry are examples of systems that implement object centric security.
For each object, one can specify among other things an owner and a series of access rules. Each access rule has a reference to an identity (who), a type of access control (allow or deny), and a set of rights specific to type of object (what). For example, for a file, you could say that “Alice” is “allowed” “read” access while “Bob” is “denied” “write” access.\
The following code snippet demonstrates how to get the owner and access rules of a file.
FileSecurity sec = new FileSecurity(fileName, AccessControlSections.All);
Debug.WriteLine("owner = " + sec.GetOwner(typeof(NTAccount)));
foreach (FileSystemAccessRule r in sec.GetAccessRules
(true, true, typeof (NTAccount)))
{
Debug.WriteLine(r.AccessControlType); // deny or allow
Debug.WriteLine(r.IdentityReference); // who
Debug.WriteLine(r.InheritanceFlags);
Debug.WriteLine(r.IsInherited);
Debug.WriteLine(r.PropagationFlags);
Debug.WriteLine(r.FileSystemRights); // specific to the file system
Debug.WriteLine("");
}
Windows defines two identities: the process (or primary) identity and the thread (or impersonated) identity. Before .NET 2.0, it was difficult to find out whether or not you were impersonating unless you used unmanaged code. One problem is that the static method WindowsIdentity.GetCurrent() return the impersonated identity if any, and the process identity otherwise.
.NET 2.0 introduces a new overloaded version where you specify that you only want the impersonated identity:
Windows.GetCurrent(bool ifImpersonating)
If you pass true, you obtain the impersonated identity, if there is one. Otherwise, the function returns null.
SecureString from .NET 2.0 is used to hold sensitive strings such as passwords. Traditional strings (System.String) potentially represent a security risk if an attacker can read the process memory. The runtime enhances security of SecureStrings by encrypting them.
Under .NET 2.0, the ProcessStartInfo type supports user name and password to launch a process using different credentials. For example, the following code starts notepad as “alice”.
System.Diagnostics.ProcessStartInfo pInfo =
new System.Diagnostics.ProcessStartInfo();
SecureString ss = new SecureString();
... append password characters
pInfo.Password = ss;
pInfo.UserName = @"domain\alice";
pInfo.FileName = @"c:\windows\notepad.exe";
pInfo.UseShellExecute = false;
Process.Start(pInfo);
When using cryptography, the hardest part is usually to manage the key. Obviously, using an unprotected key to encrypt data is useless. The challenge is to protect the original key without having to protect yet another new key.
The Data Protection API in Windows solves this problem by leveraging your login credentials. This article explains DPAPI in more details:
The new ProtectedData class exposes two static methods to encrypt and decrypt data: Protect and Unprotect.
public static byte[] Protect(byte[] userData, byte[] optionalEntropy, DataProtectionScope scope); public static byte[] Unprotect(byte[] encryptedData, byte[] optionalEntropy, DataProtectionScope scope)
In most cases, you’ll want the same current user to be able to protect and unprotect data regardless of the machine used. For this, you’ll use DataProtectionScope.CurrentUser as a third argument. But you can also pass DataProtectionScope.LocalMachine to indicate you want to use the machine credentials instead. In this case, anyone on the local machine can decrypt the data.
The optional entropy can be used to add security. For example, you can prompt the user for this entropy or store it in your application.
If you need to protect memory only for the current process or current logon, you can use the new ProtectedMemory class instead.
Data protection will be particularly useful when you want to store sensitive data in a configuration file. You can now specify that you want to protect specific sections of the configuration file.
The following snippet demonstrates how to add a protected password in the application settings:
Configuration c = Configuration.GetExeConfiguration(null,
ConfigurationUserLevel.None);
c.AppSettings.Settings.Add("Password", "secret");
c.AppSettings.ProtectSection(ProtectedConfiguration.DataProtectionProviderName);
c.Update();
The resulting configuration file contains two parts. The first section declares which part is protected and the appSettings are now encrypted.
<configuration>
<protectedData>
<protectedDataSections>
<add name="appSettings"
provider="DataProtectionConfigurationProvider" />
</protectedDataSections>
</protectedData>
<appSettings>
<EncryptedData>
<CipherData>
<CipherValue> ...encrypted info here</CipherValue>
</CipherData>
</EncryptedData>
</appSettings>
</configuration>
The framework has been modified to handle encrypted content. As a result, you
don’t have to change your code to read protected configuration.
Incidentally, you can also use an asymmetric algorithm (RSA) instead of DPAPI
to protect data.
Even when inside a firewall, it’s usually a good idea to secure the communication between machines. Indeed, the firewall can be compromised or your attacker may already be inside the firewall. Secure communication entails three aspects: authentication, message integrity and message confidentiality. To that end, Windows provides an interface (called SSPI) to several protocols including Kerberos since Windows 2000 and NTLM for last millennium versions. To simplify things, Microsoft can negotiate the best protocol between two machines.
If you want to achieve secure communication using .NET 1.1, you’d have to write your own encapsulation of SSPI, no small feat indeed. .NET 2.0 now encapsulates most of SSPI for you. While most types are internal, the visible part is the new NegotiateStream class. As the name suggests, it derives from the Stream class, which means you can use it wherever a stream is expected. An instance of NegotiateStream is always built with passing an inner stream. When you write to the NegotiateStream, it writes encrypted data to the inner stream. When you read from the NegotiateStream, it reads data from the inner stream and decrypts it.
Before sending any data, you need to authenticate: call ClientAuthenticate on the client and ServerAuthenticate on the server.
Optionally, you can specify the level of protection you need and the impersonation level.
The protection level is one of the following:
public enum ProtectionLevel
{
None = 0, // No protection
Sign = 1, // Message integrity only
EncryptAndSign = 2, // Message integrity and confidentiality
}
The default is to encrypt and sign. The impersonation level represents the power given to the other participant.
public enum TokenImpersonationLevel
{
None = 0
Anonymous = 1, // No authentication
Identification = 2, // Authentication only
Impersonation = 3, // Impersonate, local machine only
Delegation = 4, // Can impersonate and use delegation
to access resources on a remote machine
}
Identification is the default.
In addition, the new SslStream class implements SSL for TCP streams.
Secure Remoting
.NET remoting can be very fast and flexible but be aware that it is not secure by default. You have two options with .NET 1.1 and neither is ideal. The first option is to host the server in IIS and use SSL to secure the communication. The second option is to use an unsupported sample from Microsoft. (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndotnet/html/remsec.asp).
To secure remoting, .NET remoting in .NET 2.0 leverages the NegotiateStream class. It inspects the configuration files to determine the appropriate level of security. As a result, you only have to change the configuration files, not the source code.
On the server side, the new attributes are
- encryption: its value should be a valid constant for ProtectionLevel
- authenticationMode: its value should be one of the following:
public enum AuthenticationMode
{
None = 0
IdentifyCallers = 1,
ImpersonateCallers = 2,
}
On the client side, the new attributes are
- encryption: same as in the server
- impersonationlevel: its value should be a valid constant from
public enum ClientImpersonationLevel
{
None = -1
Identify = 2,
Impersonate = 3,
Delegate = 4,
}
- serviceprincipalname: represents the name of the account running the server process
For example, the following sample configuration file instructs the remoting infrastructure to expose a class as single call and to encrypt and sign the communication:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <service> <wellknown mode="SingleCall" type="Microsoft.Samples.Implementation.ImplementationClass, Server" objectUri="server.rem" /> </service> <channels> <channel ref="tcp" encryption="EncryptAndSign" port="8080"/> </channels> </application> </system.runtime.remoting> </configuration>
You’ll find a similar example in the samples that come with Visual Studio 2005 beta but the value is “SSPI” instead of “EncryptAndSign” which indicates things could change before the release.
For internet web sites, the most common way to authenticate users is to have a form collect a user name and password.
The FormsAuthentication module in .NET already provides some of the functionality: if required by the security settings, it redirects unauthorized users to a login form. Once the user is authenticated, it uses cookies to exchange the user’s information. .NET 2.0 improves this it three areas: security controls, membership and role management. In fact, in most cases, securing a web site will not involve you writing additional code.
Since most login forms tend to be the same, it makes sense to reuse components.
ASP.NET 2.0 comes with the following security controls:
Login: allows user to login and logout.
LoginView: provides different appearances based on whether
the user is logged in or logged out
LoginName: displays the name of the user if logged in, nothing
otherwise
LoginStatus: displays a login or logout button. The login button
redirects to the login page
ChangePassword: allows authenticated users to change their
password
PasswordRecovery: allows unauthenticated users to receive their
passwords by email provided they can answer a personal question
CreateUserWizard: creates a new user
Membership refers to user management: creating, modifying, deleting and authenticating users, resetting passwords, etc.
You can manage the membership information by using the web site configuration.
Programmatic access is also available through the Membership class.
Use this façade class to perform the different operations related to
users: create, modify and delete a user, authenticate a user, reset its password,
and so on.
The actual implementation is left to the membership provider classes. ASP.NET
comes with two public implementations that use SQL server and MS Access. In
both cases, the password can be store encrypted or hashed.
Once the user is authenticated, you need to assign roles. While you had to perform this by hand in .NET 1.1, it is now only a click away in .NET 2.0: select “Enable roles” in the web site configuration.
Creating and managing roles is also available programmatically through the Roles façade class. The actual implementation is left to providers. ASP.NET comes with four role providers: SQL server MS Access uses the respective databases, AuthorizationStoreRoleProvider uses authorization manager, and WindowsTokenRoleProvider leverages Windows groups.
In order to protect your machine from mobile code, Code Access Security (CAS) assigns permissions to assemblies based on their evidence (location, strong name, etc). .NET 2.0 introduces new types of demands as well as ClickOnce.
LinkDemand and InheritanceDemand can be
used when you want to protect your code based the identity of the caller or
descendant.
For example, in order to block callers unless they have a given public key k,
you would write:
[StrongNameIdentityPermission(SecurityAction.LinkDemand, PublicKey = k)]
public void Foo() {....}
But what if you require callers to have public key k1 or k2? The new LinkDemandChoice option allows you to do precisely that:
[StrongNameIdentityPermission(SecurityAction.LinkDemandChoice,
PublicKey = k1)]
[StrongNameIdentityPermission(SecurityAction.LinkDemandChoice,
PublicKey = k2)]
public void Foo() {....}
DemandChoice and InheritanceChoice are also available.
While developers think about developing assemblies, administrator and users worry about deploying and using applications. In order to facilitate application management, the new ClickOnce XML manifest describes the application. Its file name is the name of the executable with a “.manifest” suffix. Among other features, it enumerates the permissions the application requires. When the application runs using ClickOnce, it obtains precisely the permissions listed the manifest – no more, no less.
When building the manifest, you can choose one of the predefined CAS permission sets. Alternatively, you can calculate the required permissions based on what your code does. The permcalc tool examines your code and gathers all the required permissions. You can also access it through the “calculate” button in the security tab of the application properties.
When users download a ClickOnce application, the runtime will check that the required permissions are a subset of the permissions granted by the policy. Otherwise, it will ask the user whether he/she wants to elevate the permissions of the application. If the user agrees, the runtime trusts the application and modifies the policy accordingly.
.NET 2.0 exposes significantly more security material than .NET 1.1. By providing new functionality out of the box, it enables regular developers to build very secure applications more quickly. Once .NET 2.0 is out, it will be tough to live without! In the meantime, take advantage of the beta version.