
December 6, 2004
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.
Certificates and certificate stores
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);
}
Public Key Cryptography Standard
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.
Better Windows security
.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.
Accounts and security identifiers
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.
Object centric security
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("");
}
Getting the impersonated identity
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 for passwords
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.
Launching a process with different credentials
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);
Protecting data
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:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnsecure/html/windataprotection-dpapi.asp
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.
Protecting configuration
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.
Secure communication using NegotiateStream
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.
ASP.NET: More functionality in the framework, less coding for you
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.
Login controls
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 management
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.
Role management
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.
Code access security
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.
Demand choice
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.
ClickOnce
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.
Conclusion
.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.
Authors
 |
Pierre is a software consultant in the San Francisco area. He specializes in all areas of the .NET platform.
He has experience in data access, object-oriented programming, component architecture, and compiler technology.
He is the author of OLE DB Consumer Templates: A Programmer's Guide published by Addison-Wesley.
He is also the creator of XC#, an extensible C# compiler.
|
|