Chapter 10. Extending the CLR Security System to Protect Your Extensible Application

Microsoft .NET Framework applications run in a variety of different environments with a range of security considerations. For example, an environment in which code is dynamically downloaded to a machine and executed has different security considerations than a lockeddown environment in which only code that has been explicitly approved and installed by an administrator is allowed to run. Because the CLR supports application models that allow code to be run in such varied environments, the CLR security system must be flexible enough to enable the providers of application models to enforce the security constraints they desire.

Throughout this book, I’ve talked about the techniques you can use to define new application models for managed code by writing CLR hosts and other extensible applications. Defining a new application model necessarily requires you to think about the security requirements for the environment in which your application will be running. A solid security design is critical for an extensible application primarily because much of the code that is loaded into an extensible application comes from unknown sources. The add-ins that will be loaded into your process will most likely be written by someone other than yourself. As a result, you don’t know the origin of the code, which .NET Framework APIs it uses, whether it calls out into unmanaged code, and so on. As you’ll see throughout this chapter, you can specify which operations can be performed by code running in the application domains you create. It’s best to take a conservative approach to security when writing an extensible application that will execute assemblies from unknown sources. Securing your process from malicious access is important not only to protect your own data, but also to protect access to resources on the machine on which your application is running.

Fortunately, extensibility has been a key design goal of the CLR security system from the beginning. Nearly all of the system’s core features can be extended and customized by individual applications. Throughout this chapter, I show you how to take advantage of these extensibility points to secure your extensible application. I begin by providing an overview of the CLR Code Access Security (CAS) system and the various ways it can be extended. Then I use these concepts to add security features to the cocoon deployment host I built in Chapter 8.

An Overview of Code Access Security

A thorough description of the CAS system is beyond the scope of this chapter. CAS is so broad and so detailed that entire books have been dedicated to the subject. In this section, I focus on the three main concepts in CAS (evidence, policy, and permissions) and describe them from the perspective of their use in extensible applications.

Perhaps the best way to begin describing CAS is by looking back at the original motivation behind the creation of the feature and the scenarios it is meant to enable. The primary goal of CAS is to enable what is referred to as partially trusted code. In the partial trust model, the range of operations that code can perform is related to how much it is trusted by the machine administrator and by the host that owns the process in which it is running. The level of trust, and therefore the operations the code can perform, can be very granular and ranges from no trust to fully trusted. Code with no trust is completely restricted—it can’t even run. Code that is fully trusted is allowed to do anything it wants. Allowing levels of trust between full and none is where CAS provides its value. For example, a machine administrator might dictate that a given piece of code can access only a certain portion of the file system and display some user interface, but it can’t do anything else, such as access the network, print documents, or store data in the registry.

Indeed, the motivation behind CAS and the partial trust model is to provide a more granular model than the "all or nothing" model used in many existing operating system security models. Consider the case of a control that is downloaded from an Internet site and executed on a local machine. In the purely native code security model, the administrator (and the user) have only two choices when determining how much to trust the control. They can decide either to trust it completely or not trust it at all. Trusting the control completely allows the control to have full access to the machine. Not trusting the control means it won’t even be installed. The .NET Framework recognizes that there are many scenarios in which a more middle-of-the-road approach is desired. That is, code can be trusted to perform some operations but not others. These are the scenarios that CAS enables.

In addition to providing a more granular trust model, CAS differs from the existing Microsoft Windows operating system security model in another key way: security decisions in CAS are based on code identities, not user identities. That is, CAS grants code the permission to perform certain operations based on characteristics of the code itself, not based on the user who is logged on to the machine when the code is running. For example, an administrator or host can grant code the permission to access the file system based on whether the assembly has a strong name associated with a particular cryptographic key pair. In contrast, administrators of the Windows security system make decisions about which portions of the file system can be accessed based on the user who is running the code. The characteristics of the code that can be used to make security decisions in CAS are called evidence. Evidence is one of the primary concepts in CAS, as you’ll see in the next section.

The Core Concepts: Evidence, Permissions, and Policy

The CAS system is built around the following three primary concepts: evidence, permissions, and policy.

  • Evidence. As described, security decisions in CAS are based on characteristics of the code called evidence. Evidence is associated with an assembly and comes from two main sources: the CLR and the extensible application that is loading the assembly.

  • PermissionsA permission represents the right to access a given resource in the system. For example, the .NET Framework includes a permission called FileIOPermission that defines which portion of the file system an assembly can access. Authors of extensible applications can also define their own permissions to protect access to application-specific resources.

  • Policy. Policy is the mapping between evidence and permissions. CAS policy is a collection of rules (expressed in what are called code groups) that describe which permissions are granted to the assembly based on the evidence associated with that assembly. For example, a policy statement can specify that "all code downloaded from www.cohovineyard.com can execute on my machine, but it cannot access any resources such as the file system or registry." Policy can be defined both by the administrator of the machine and the extensible application that hosts the assembly.

These concepts all work together to determine the set of operations an assembly can perform. Evidence is the input to policy, which specifies which permissions are granted as shown in Figure 10-1.

Code Access Security policy grants permissions to an assembly based on evidence.

Figure 10-1. Code Access Security policy grants permissions to an assembly based on evidence.

As you’ve seen, all three of these primary concepts are extensible. In the next few sections, I describe these concepts in more detail and talk about when you might want to define custom evidence, write a custom permission, or author CAS policy statements for an extensible application.

Evidence

As discussed, the evidence describing an assembly comes from two main sources: the CLR and the extensible application that is hosting the assembly. The CLR assigns a fixed set of evidence to an assembly when it is loaded. This evidence can include the assembly’s location on the file system or on the Internet, its strong-name signature, the identity of the assembly’s publisher, and so on. Evidence assigned by the host can include these forms of evidence as well, but a host can also define application-specific evidence classes. For example, later in the chapter, I define application-specific evidence that identifies an assembly as contained in a cocoon file from the sample built in Chapter 8.

Evidence, like all of the core concepts in the CAS system, is implemented as managed objects. The evidence that is natively understood by the CLR is represented by managed classes, including the following from the System.Security.Policy namespace:

  • Application Directory

  • Gac

  • Hash

  • PermissionRequestEvidence

  • Publisher

  • Site

  • StrongName

  • Url

  • Zone

Note

Note

An assembly can also provide evidence about itself. However, this evidence is weaker in the sense that any evidence provided by the CLR or the hosting application can always override it.

Let me make the concept of evidence more concrete by giving a specific example. The evidence for a given assembly can be discovered using the Evidence property on System. Reflection.Assembly. The Evidence property is an object of type System.Security.Policy.Evidence that contains a collection of all the evidence associated with an assembly. The program in Example 10-1 loads an assembly and enumerates its evidence using the Evidence property from the Assembly class.

Example 10-1. Evidencedisplay.cs

using System;
using System.Text;
using System.Reflection;
using System.Security.Policy;
using System.Collections;
namespace EvidenceDisplay
{
   class Program
   {
      static void Main(string[] args)
      {
         // Load an assembly.
         Assembly a = Assembly.Load("Utilities");

         // Get and display its evidence.
         Evidence ev = a.Evidence;
         IEnumerator e = ev.GetEnumerator();

         while (e.MoveNext())
         {
            Console.WriteLine(e.Current);
         }
       }
    }
 }

The preceding program generates the following output:

<System.Security.Policy.Zone version="1">
<Zone>MyComputer</Zone>
</System.Security.Policy.Zone>

<System.Security.Policy.Url version="1">
<Url>file:///C:/temp/Evidence/Evidence/bin/Debug/Utilities.dll</Url>
</System.Security.Policy.Url>

<StrongName version="1"
Key="0024000004800000940000000602000000240000525341310004000001000100571ED9EF397800C456148B4
CB3F5F1DC73223B883C62E1A7804E80CA2084FEE41D26B233AAF044BA8D6322D1BD78E448F07DFD4B06510A2C87D
1D7DC86F89EAE304A327737B290B9AC20BEB84F132C8B95A7868A8938562027803333381D8DD2A9E4D66A41E1A83
D01F7CE5C01DAC8A4CB9FBD02EEBEBEAB870D8EB291E4FCA6"
Name="Utilities"
Version="1.0.1613.31500"/>

<System.Security.Policy.Hash version="1">
<RawData>4D5.......0000000000000</RawData>
</System.Security.Policy.Hash>

As you can see, the CLR has assigned evidence to the Utilities assembly describing the zone and file location from which it originated, its strong-name signature, and a hash of the assembly’s contents (I’ve abbreviated the output to display only a portion of the hash).

Evidence on its own merely describes an assembly. To make any use of evidence, policy must be defined that maps that evidence to a set of permissions. The next section describes CAS policy in more detail, including its role in extensible applications.

Policy

CAS policy is expressed as a collection of code groups that define the mapping between evidence and the set of permissions to grant the assembly. Each code group consists of a membership condition and a policy statement. A membership condition is a qualifier that defines the specific condition that an assembly’s evidence must satisfy to be granted the specified set of permissions. The set of permissions to be granted, along with some attributes that describe how these permissions are merged with those from other code groups, is specified in the policy statement. Figure 10-2 shows a sample code group. This group grants all code downloaded from www.cohowinery.com the permission to execute and to read and write to the c: emp directory in the file system.

Code groups consist of a membership condition and a policy statement.

Figure 10-2. Code groups consist of a membership condition and a policy statement.

The code groups that constitute CAS policy are arranged in a hierarchy. That is, the code groups form a tree in which a code group can have other code groups as children. The code group at the top of a CAS policy tree always has a membership condition of all code, meaning that every assembly qualifies regardless of its evidence. The All Code code group then has children that define the mappings between specific pieces of evidence and a permission set to grant. An example of a policy tree is shown in Figure 10-3.

Code groups are arranged in a hierarchy.

Figure 10-3. Code groups are arranged in a hierarchy.

When evaluating policy for a given assembly, the CLR takes the assembly’s evidence and compares it against the membership conditions for all code groups in the policy tree. The permission set specified by each code group for which the assembly’s evidence qualifies is added to the overall set of permissions that will be granted to the assembly. For example, the policy tree in Figure 10-3 contains two code groups (other than the All Code group). One group (the Coho Winery code group) grants permissions based on the fact that an assembly originated from www.cohowinery.com. The second code group (the Coho Strong Name code group) grants permissions based on whether the given assembly was signed with a specific strong-name key (the Coho Winery key).

To illustrate how the permission sets from multiple code groups are typically combined, assume the CLR is evaluating policy for an assembly from www.cohowinery.com that is signed with the Coho Winery key. The evaluation process begins at the top of the tree. After satisfying the membership condition for All Code, the CLR asks the Coho Winery code group whether the assembly’s evidence passes the URL membership condition specified in that code group. Because the assembly originated from www.cohowinery.com (recall that the CLR automatically assigns this evidence), the permissions that grant the right to execute and to read and write from c: emp are added to the assembly’s grant set. Next, the CLR tests the membership condition for the Coho Strong Name code group. This membership condition passes as well because the assembly is signed with the proper key. As a result, the permission to read and write anywhere on c: and the permission to programmatically access the www.cohowinery.com site are granted to the assembly as well. The overall set of permissions granted to the assembly includes the following rights:

  • Execute

  • Read and Write to c: (this is a superset of the permission to read and write only from c: emp)

  • Access www.cohowinery.com

In this example, the permissions granted by each qualifying code group were combined (using the mathematical union operation) to form the final grant set. It’s typically the case that permission sets are combined in this fashion, but other options are possible. The behavior for how different permission sets are combined is determined by the types of code groups in the policy tree and by the attributes on their policy statements. In our example, all code groups were union code groups. However, there are other types of code groups, including first match code groups, which define different rules for how permission sets are combined.

As with all of the core concepts in CAS, the code groups and membership conditions discussed here are represented as managed objects. The .NET Framework provides membership conditions that correspond to the types of evidence natively understood by the CLR. For example, the ZoneMembershipCondition class in System.Security.Policy corresponds to Zone evidence, the StrongNameMembershipCondition is used to test StrongName evidence, and so on. In addition, the CAS policy system is extensible and allows custom code groups and custom membership conditions to be defined. I take advantage of this capability later in the chapter when I define a custom membership condition to test whether evidence is present that identifies an assembly as coming from a cocoon file.

Policy Levels

The hierarchy of code groups just described constitutes a policy level. The CAS policy system has four such levels:

  • Enterprise. Enterprise CAS policy is specified by a system administrator, typically using the .NET Framework Configuration tool, and deployed to the machines in an enterprise using any number of software distribution tools.

  • Machine. Administrators can also define CAS policy that applies to all applications running on a given machine. Machine-level CAS policy is also typically specified using the .NET Configuration tool.

  • UserCAS policy can also be specified for particular users on a machine. This level of policy enables an administrator to grant different sets of permissions to different types of users, for example.

  • Application domainCAS policy can be specified by the creator of each application domain. By default, each new domain has no such policy. The ability to provide custom policy per application domain is one of the primary techniques an extensible application can use to customize the CAS system. Later in the chapter, I define application domain CAS policy for the domains I create as part of the cocoon CLR host.

The CLR evaluates all four of these policy levels to determine the set of permissions that should be granted to an assembly. The four grant sets that result from evaluating the individual policy levels intersect to determine the assembly’s final grant set as shown in Figure 10-4.

The grant sets from each policy level intersect to determine the set of permissions granted to the assembly.

Figure 10-4. The grant sets from each policy level intersect to determine the set of permissions granted to the assembly.

The intersection of the grant sets from the individual policy levels results in a least-common-denominator approach to determining the final set of permissions granted to the assembly. As a result, no level can force a particular permission to be granted. A permission must be granted by all other levels for it to be part of the final grant set.

The caspol.exe tool that ships with the .NET Framework SDK contains some options you can use to evaluate the enterprise, machine, and user policy levels statically for a given assembly. These options are useful both to understand how the policy system works in general and to diagnose why the policy system isn’t granting an assembly the set of permissions you expect. The -rsp (stands for "resolve permissions") flag to caspol.exe displays the results of evaluating policy for the assembly you provide. For example, the following output was generated by running Caspol–rsputilities.dll from a command prompt:

Microsoft (R) .NET Framework CasPol 2.0.40301.9
Copyright (C) Microsoft Corporation 1998-2004. All rights reserved.

Resolving permissions for level = Enterprise
Resolving permissions for level = Machine
Resolving permissions for level = User

Grant = <PermissionSet class="System.Security.PermissionSet"
   version="1">

<IPermission class="System.Security.Permissions.EnvironmentPermission,
   mscorlib, Version=2.0.3600.0, Culture=neutral,
   PublicKeyToken=b77a5c561934e089"
   version="1"
   Read="USERNAME"/>

<IPermission class="System.Security.Permissions.FileDialogPermission,
   mscorlib, Version=2.0.3600.0, Culture=neutral,
   PublicKeyToken=b77a5c561934e089"
   version="1"
   Unrestricted="true"/>

<IPermission class="System.Security.Permissions.SecurityPermission,
   mscorlib, Version=2.0.3600.0, Culture=neutral,
   PublicKeyToken=b77a5c561934e089"
   version="1"
   Flags="Assertion, Execution, BindingRedirects"/>

<IPermission class="System.Security.Permissions.UIPermission,
   mscorlib, Version=2.0.3600.0, Culture=neutral,
   PublicKeyToken=b77a5c561934e089"
   version="1"
   Unrestricted="true"/>

<IPermission class="System.Drawing.Printing.PrintingPermission,
   System.Drawing, Version=2.0.3600.0, Culture=neutral,
   PublicKeyToken=b03f5f7f11d50a3a"
   version="1"
   Level="DefaultPrinting"/>

<IPermission class="System.Diagnostics.EventLogPermission,
   System, Version=2.0.3600.0, Culture=neutral,
   PublicKeyToken=b77a5c561934e089"
   version="1">
   <Machine name="." access="Instrument"/>
</IPermission>

</PermissionSet>

As you can see from this output, the intersection of the enterprise, machine, and user policy levels grants the Utilities assembly the permission to read the USERNAME environment variable, display user interface, use the printer, write to the event log, and so on .

Note

Note

As discussed, the core concepts of CAS are all represented as managed objects. So their on-disk representation is just the serialized form of the managed object. You can see this representation both in the preceding output from caspol.exe and in the output from the evidence display program listed earlier in the chapter. Running caspol –rgs determines the final grant set for the assembly and outputs it using .NET serialization. The policy trees that represent the enterprise, machine, and user policy are also serialized to disk in this way. For example, you can look at the serialized form of the machine policy tree by looking at the security.config file in the %windir%microsoft.netframeworkv2.0.41013config directory. The .NET Configuration tool simply edits the managed objects in memory and persists them to disk files using .NET Framework serialization.

Although caspol.exe can’t be used to evaluate application domain CAS policy, it is useful to help debug extensible applications nonetheless. For example, you might expect that the code in your application domain should get a specific set of permissions as specified by your application domain CAS policy. However, if the enterprise, machine, or user level doesn’t also grant the permission you do at the application domain level, the permission will not be included in the assembly’s final grant set. Using caspol.exe to evaluate the grant set produced at the enterprise, machine, and user levels is a great way to diagnose these sorts of issues.

Default CAS Policy

The .NET Framework ships with default security policy for the enterprise, machine, and user levels. Generally speaking, default policy grants full trust to all code that is installed on the local machine and to all assemblies that ship as part of the .NET Framework. Code that is loaded from the Internet, an intranet, or file shares gets a reduced set of permissions. As demonstrated, it’s useful to understand the contents of the enterprise, machine, and user levels of policy because policy does have an effect on the permission you’ll grant as part of your extensible application. CAS policy, whether it’s the default policy shipped with the .NET Framework or a set of policy explicitly provided by an administrator, is best viewed with the .NET Configuration tool. Figure 10-5 shows the code group hierarchy representing machine policy as displayed in the Configuration tool.

The .NET Configuration tool makes it easy to view security policy for the enterprise, machine, and user levels.

Figure 10-5. The .NET Configuration tool makes it easy to view security policy for the enterprise, machine, and user levels.

Permissions

As described, a permission represents the right to access a particular resource. Permissions are implemented as managed objects just as the other core concepts of the CAS system are. As demonstrated, permissions are granted to an assembly as a result of evaluating CAS policy based on the evidence associated with that assembly. In the next section, I discuss how the CLR enforces that the resources protected by a permission cannot be accessed by unauthorized code.

The .NET Framework includes several permissions that protect the resources on a typical computer. I gave some examples of these permissions earlier when I discussed the results of evaluating security policy for a particular assembly using caspol.exe. The list of permissions included with the .NET Framework includes the following:

  • FileIOPermission

  • RegistryPermission

  • EnvironmentPermission

  • ReflectionPermission

  • WebPermission

  • SocketPermission

  • UIPermission

  • SecurityPermission

As with most aspects of the CAS system, the set of permissions that can be granted to an assembly is completely extensible. If you have a resource to protect that isn’t covered by one of the .NET Framework permissions, you simply write your own. The permissions you write can be granted by CAS policy and are enforced by the CLR just as the .NET Framework permissions are. I won’t cover the details of how to write a custom permission in this book, but more information and examples can be found in the .NET Framework SDK and in any of a number of books dedicated to CAS.

Runtime Enforcement of Permissions: Permission Demands and the Stack Walk

At a high level, a security system performs the following three activities to ensure the protection of a particular resource:

  1. Authentication

  2. Authorization

  3. Enforcement

Authentication, or the secure identification of the entity attempting to access a protected resource, is accomplished in CAS through the assignment of evidence to an assembly. The assignment of rights to the secure identity, or authorization, corresponds to the evaluation of CAS policy. In the last few sections, I discussed how evidence is used by the policy system to determine the set of permissions an assembly is granted. In this section, I show how the CLR enforces that a given assembly can access only the resources for which it has permission.

The enforcement of permissions by the CLR is done using three techniques: validation, verification, and stack walks. Validation and verification refer to the steps taken to ensure the correctness of an assembly. Validation ensures that the assembly’s metadata and intermediate language (IL) stream are well formed. That is, the metadata doesn’t include pointers to random memory locations in the file and that all IL instructions performed by the assembly are correctly formed. Verification ensures that the code in the assembly is type safe. In Chapter 5 I discuss the importance of type safety in ensuring that the isolation boundary provided by an application domain is sound. The final technique used to enforce the correctness of the CAS system is the stack walk. Simply put, a stack walk ensures that the assembly attempting to access a particular resource (and all of its descendents on the call stack) has been granted the permission required to access the resource.

The process of enforcing permissions through a stack walk begins with the demand of a permission by the class library that provides the managed API over the protected resource. This process works as follows. Resources protected by CAS permissions always have a managed API that is used to access them. For example, the .NET Framework includes a set of APIs to access the file system. These APIs, contained in the System.IO namespace, protect access to the file system using the FileIOPermission. Similarly, the .NET Framework APIs for accessing the registry protect the underlying resource using RegistryPermission. These class libraries protect resources by issuing a demand for the appropriate permission. For example, Figure 10-6 shows a class library containing a class called File that applications use to access the file system. The Read method on the File class issues a demand for FileIOPermission before reading the contents of the file.

Class library authors demand permissions to protect resources.

Figure 10-6. Class library authors demand permissions to protect resources.

A permission demand is an indication to the CLR that it should check to make sure that the caller attempting to access the resource has been granted the appropriate permission. In other words, the demand of a permission initiates a stack walk. When walking the stack, the CLR checks the assembly issuing the demand and all other assemblies in the call stack to make sure that they have been granted the appropriate permission, FileIOPermission, in this case. If all assemblies have the proper grant, the demand passes and execution proceeds. However, if any assembly in the call chain has not been granted the required permission, execution stops and an instance of System.SecurityException is thrown. The process of walking the stack in response to a demand for a permission is shown in Figure 10-7.

The CLR walks the stack to ensure all callers have been granted the demanded permission.

Figure 10-7. The CLR walks the stack to ensure all callers have been granted the demanded permission.

In the general case, a stack walk involves checking all callers on the stack as I’ve just described. However, CAS involves various concepts that can introduce variations on the basic stack walk. For example, demands such as link demands and inheritance demands check only an assembly’s immediate caller instead of all callers on the stack. In addition, you can use various APIs to control how the stack walk is performed. Examples of these stack walk modifiers include the assert, the deny, and the permit only.

There is much more to the CAS system than what I have presented here. However, this introduction should provide enough background for you to be able to add the basic CAS concepts to an extensible application. Now that I’ve covered the basics, take a look at the specific APIs the .NET Framework provides for customizing evidence, permissions, and policy.

Customizing the Code Access Security System Using a HostSecurityManager

The HostSecurityManager class in the System.Security namespace provides the infrastructure through which an extensible application can customize the CAS system for individual application domains. By providing a class derived from HostSecurityManager, an extensible application can assign evidence to assemblies as they are loaded, supply a CAS policy tree for an application domain, and so on. Table 10-1 lists the members of HostSecurityManager.

Table 10-1. The Members of HostSecurityManager

Method

Description

DomainPolicy

A property through which an extensible application can supply an application domain CAS policy level.

ProvideAssemblyEvidence

A method used to supply evidence to assemblies as they are loaded. Extensible applications implement this method to supply hostspecific evidence to augment the evidence provided by the CLR.

DetermineApplicationTrust

A method by which extensible applications can decide whether an application defined by a formal manifest is allowed to run. Note: I don’t cover the topic of application manifests at all in this book. Refer to the .NET Framework SDK for more information on this property.

Flags

A set of flags of type HostSecurityManagerFlags through which the extensible application tells the CLR which CAS customizations it is interested in supplying. For example, the CLR won’t consult your HostSecurityManager implementation for an applicationdomain-level policy tree unless this property includes the HostSecurityManagerFlags.HostPolicyLevel flag.

A HostSecurityManager is part of the application domain manager infrastructure that extensible applications use to customize new application domains. When initializing a new application domain, the CLR checks to see whether an implementation of HostSecurityManager has been provided for the new domain by accessing the HostSecurityManager property on the domain’s application domain manager.

Now that I’ve covered the core concepts of the CAS system and have taken a first look at the HostSecurityManager class, I will go ahead and write an extensible application that extends CAS to enforce application-specific security requirements.

Code Access Security in the Cocoon Host

In Chapter 8, I wrote a CLR host called runcocoon.exe that enabled a new deployment model called a cocoon. Recall that the cocoon deployment model allowed you to package all the files in your application (minus the .NET Framework assemblies) into a single OLE-structured storage file. Runcocoon.exe customized how the CLR loads assemblies by providing an assembly loading manager using the CLR hosting interfaces to run the applications contained in cocoon files. In this section, I extend the host built in Chapter 8 to use the CAS system to restrict the set of operations that can be performed by applications contained in cocoons.

Before I get into the implementation details of how to customize CAS, allow me to establish some security requirements for runcocoon.exe. In particular, the enhancements I make in this chapter will enforce that code running as part of a cocoon application has the following characteristics:

  • It can reference only other assemblies contained in the cocoon and the .NET Framework assemblies. No other assemblies will be granted the permission to execute. A SecurityException will be raised if an attempt is made to load such an assembly.

  • It will have permission only to execute, display user interface, and read and write files to a temporary scratch space referred to as isolated storage. (I describe a bit more about isolated storage later in the chapter when I build the policy tree that grants the appropriate permission.)

I use the infrastructure provided by a HostSecurityManager to enforce these requirements. In particular, I supply an application domain CAS policy tree that grants the assemblies in the cocoon the permission only to execute, display user interface, and manipulate isolated storage. The assemblies in the cocoon will be identified to the CAS system using custom evidence and a custom membership condition. Modifying runcocoon.exe to incorporate CAS requires the following steps:

  1. Create an initial implementation of a class derived from HostSecurityManager.

  2. Create custom evidence used to authenticate the assemblies in the cocoon.

  3. Create a membership condition that can recognize the custom evidence.

  4. Create an application-domain-level CAS policy tree that grants the appropriate permissions.

  5. Assign the custom evidence to the assemblies in the cocoon.

The following sections describe these steps in detail.

Step 1: Provide an Initial Implementation of HostSecurityManager

The customizations I’d like to achieve are centered around the implementation of a host security manager. Plugging the initial implementation of a host security manager into the CAS system requires three initial steps: (1) derive a class from the HostSecurityManager base class, (2) implement the Flags property to tell the CLR which customizations I am interested in participating in, and (3) return an instance of the host security manager from the application domain manager.

The host security manager will participate in two of the customizations offered by HostSecurityManager: it provides custom evidence for assemblies and an application-domain-level CAS policy tree. So I must return the HostSecurityManagerFlags.HostAssemblyEvidence and HostSecurityManagerFlags.HostPolicy-level flags from the host security manager’s implementation of the Flags property. The initial implementation looks like this:

public class CocoonSecurityManager : HostSecurityManager
{
   public override HostSecurityManagerFlags Flags
   {
      get
      {
         return (HostSecurityManagerFlags.HostAssemblyEvidence |
            HostSecurityManagerFlags.HostPolicyLevel);
      }
   }
}

Providing the CLR with an instance of the host security manager is just a matter of creating a new instance of CocoonSecurityManager and returning it from the HostSecurityManager property of the application domain manager. The following snippet shows the application domain manager class implemented in Chapter 8 with an implementation of the HostSecurityManager property added.

public class CocoonDomainManager : AppDomainManager, ICocoonDomainManager
{
   public override HostSecurityManager HostSecurityManager
   {
      get
      {
         // Return a new instance of the security manager.
         return new CocoonSecurityManager();
      }
   }
   // The rest of the class omitted...
}

Now that I’ve got the basic infrastructure in place, I will go ahead and fill in the details of the implementation.

Step 2: Create Custom Evidence

Recall that the CLR associates various types of evidence, including evidence describing the assembly’s origin and any signatures it might have, with an assembly automatically as the assembly is loaded. However, none of the evidence the CLR assigns by default is sufficient to tell you that an assembly was loaded from a cocoon file, so I implement my own evidence for this purpose.

Implementing the custom evidence is very straightforward because any managed class can be used to represent evidence associated with an assembly. The custom evidence I implement in the runcocoon.exe host is a simple managed type called EvCocoon:

public class EvCocoon
{
};

The real work comes in implementing a membership condition to recognize this evidence and in constructing a code group that uses that membership condition to assign permissions, as you’ll see in the next few sections.

Step 3: Create a Custom Membership Condition

Classes that represent custom evidence aren’t generally useful without a corresponding membership condition that recognizes the evidence during policy evaluation. Earlier in the chapter, I discussed how the .NET Framework provides membership conditions that recognize the various types of evidence that the CLR natively understands, such as strong names, URLs, and so on. In this section, I do the same by providing a membership condition called CocoonMembershipCondition that recognizes the EvCocoon custom evidence object.

All membership conditions must implement the IMembershipCondition interface. IMembershipCondition derives from two other interfaces: ISecurityEncodable and ISecurityPolicyEncodable. The members on IMembershipCondition are shown in Table 10-2.

Table 10-2. The Methods on IMembershipCondition

Method

Description

Check

Given a collection of evidence, Check looks to see whether the collection contains the evidence the membership condition is looking for. In the sample implementation, Check will look for evidence of type EvCocoon.

Copy

Returns an exact copy of the instance of the membership condition on which it is called.

ToXml from ISecurityEncodable

Provides an XML representation of the membership condition.

FromXml from ISecurityEncodable

Creates the membership condition from the XML representation.

ToXml(policyLevel) from ISecurityPolicyEncodable

Provides an XML representation of the membership condition specific to the requested policy level.

ToXml(policyLevel) from ISecurityPolicyEncodable

Creates the membership condition from the XML representation for a specific policy level.

The Check method is the heart of a membership condition. The CLR calls Check during policy resolution to determine whether the set of evidence associated with an assembly satisfies the criteria required by the membership condition. If Check returns true, the CLR adds the permissions granted by the code group containing the membership condition. The implementation of Check in CocoonMembershipCondition enumerates the collection of evidence passed in looking for an instance of EvCocoon. The implementation of CocoonMembershipCondition is shown in the following listing:

public class CocoonMembershipCondition : IMembershipCondition
   {
      public bool Check(Evidence evidence)
      {
         if (evidence == null)
            return false;

         // Loop through the evidence looking for an instance of
         // EvCocoon.
         IEnumerator enumerator = evidence.GetHostEnumerator();
         while (enumerator.MoveNext())
         {
            Object obj = enumerator.Current;

            if (obj is EvCocoon)
            {
               // We've found cocoon evidence!
               return true;
            }
         }
         return false;
      }

      public IMembershipCondition Copy()
      {
          return new CocoonMembershipCondition();
      }

      public override bool Equals(Object o)
      {
         CocoonMembershipCondition that = (o as CocoonMembershipCondition);

         if (that != null)
         {
             return true
         }
         return false;
      }

      // The Cocoon membership condition cannot be specified in
      // security XML configuration files.
      public SecurityElement ToXml()
         { throw new NotSupportedException(); }
      public void FromXml(SecurityElement e)
         { throw new NotSupportedException(); }
      public SecurityElement ToXml(PolicyLevel level)
         { throw new NotSupportedException(); }
      public void FromXml(SecurityElement e, PolicyLevel level)
         { throw new NotSupportedException(); }
   };

The ToXml and FromXml methods are called by the CLR during policy administration to translate the membership condition to and from an XML representation. You must implement these methods if you’d like your membership condition to be included in the enterprise, machine, and user policy levels because the definition of those policy levels is persisted to XML files stored on disk. In the sample case, however, the CocoonMembershipCondition will appear only in the custom application-domain-level policy tree. So I’ve chosen not to implement the methods required to translate a CocoonMembershipCondition to and from XML.

Step 4: Create an Application-Domain-Level Policy Tree

The custom evidence and custom membership condition I’ve built in the last few sections provide all the pieces I need to create a CAS policy tree that grants the appropriate permissions to assemblies loaded out of a cocoon.

A central element of the policy tree is a code group that grants the appropriate permissions to all assemblies that pass the membership condition implemented by CocoonMembershipCondition. Recall that I want assemblies in cocoon files to be able to execute, display user interface, and store files using isolated storage. These three permissions are represented, respectively, by the SecurityPermission, UIPermission, and IsolatedStorageFilePermission permissions from the System.Security.Permissions namespace. These permissions are grouped together into a PermissionSet object that is passed, along with an instance of the CocoonMembershipCondition class, to the constructor of the System.Security.Policy.UnionCodeGroup as shown in the following snippet:

// Create the permission set granted to assemblies that satisfy
// the custom membership condition. Grant the permission to execute,
// display UI, and access IsolatedStorage.
PermissionSet pSet = new PermissionSet(PermissionState.None);

// Add permission to execute.
pSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));

// Add permission to display UI.
pSet.AddPermission(new UIPermission(PermissionState.Unrestricted));

// Add permission to store 10k of data in isolated storage.
IsolatedStorageFilePermission isoStorePermission = new
 IsolatedStorageFilePermission(PermissionState.None);
isoStorePermission.UsageAllowed =
 IsolatedStorageContainment.DomainIsolationByUser;
isoStorePermission.UserQuota = 10000;
pSet.AddPermission(isoStorePermission);

// Create a code group with the custom membership condition and grant set.
UnionCodeGroup cocoonCG = new UnionCodeGroup(
   new CocoonMembershipCondition(), new PolicyStatement(pSet));

The policy tree needs more than just this one code group, however. Recall that the set of permissions granted to an assembly is the intersection of the permission grants from each of the four policy levels, so I must grant a set of permissions to all assemblies allowed to run in the application domain. Although the code group I’ve just built covers the assemblies in the cocoon, I must also add code groups that grant permissions to the .NET Framework assemblies. Without such a code group, the .NET Framework assemblies wouldn’t be granted any permissions and, hence, wouldn’t be allowed to run, even though the default security policy has granted them full trust! In addition, the host runtime assembly (CocoonHostRuntime) must also have permission to run in the application domain. Given the need to grant permissions to these other assemblies, the final policy will have several other code groups, as shown in Figure 10-8.

The application domain policy level for the runcocoon.exe host

Figure 10-8. The application domain policy level for the runcocoon.exe host

As you can see in the figure, the policy tree contains code groups that grant full trust to the .NET Framework and CocoonHostRuntime assemblies based on the public key pair that was used to generate their strong names. This is accomplished in code using instances of System.Security.Policy.StrongNameMembershipCondition. Creating an instance of StrongNameMembershipCondition requires the public key used when the assembly was signed. The sn.exe utility from the .NET Framework SDK has options that enable you to extract the public key from a strong-named assembly and format that key in a way that makes it easy to paste into source code as an array of bytes. For example, to obtain the public key used for CocoonHostRuntime, I’d use the -e option:

C:sn –e CocoonHostRuntime.dll CocoonPublicKey.snk

Now, given the key contained in cocoonpublickey.snk, I can use the -o option to translate that key into a form that’s easy to use in source code:

C:sn –o CocoonPublicKey.snk CocoonPublicKey.csv

Finally, I can define the public key in source code using the contents of cocoonpublickey.csv like this:

private static byte[] s_CocoonHostRuntimePublicKey =
{
    0,  36,   0,   0,   4, 128,   0,   0, 148,   0,   0,   0,
    6,   2,   0,   0,   0,  36,   0,   0,  82,  83,  65,  49,
    0,   4,   0,   0,   1,   0,   1,   0, 241, 255, 223,  68,
    103,  53,  57, 194,  68, 246,  41,  44, 219, 236, 159,  34,
    224, 176, 134, 172, 137,  77,  26, 145, 228, 143, 130,  16,
    75,  36, 135,  78, 188, 240,  60, 158, 191,  99, 180,  73,
    195, 154,  43,  24, 231, 230,  59,  49, 123, 233,  45, 148,
    56,   6, 192,  62, 100, 214,  15, 121,   2, 187, 167,  54,
    124,  15, 222,  25, 189, 129, 195,  28, 141, 227, 254, 209,
    189, 241,  48, 114, 192, 210, 132, 218,  80,  70, 248, 240,
    163,  79, 121, 196,  44,  83,  64, 217,  55,  19,  31, 204,
    104, 138,  91,  82, 208,  10,  72, 112, 214,  44, 127,  47,
    186,  72,  80, 101, 227, 240, 184,  27, 181,  50, 137, 147,
    173, 222, 101, 231
};

Given the definitions of the necessary public keys, all that’s left is to construct the final policy tree and return it from the host security manager’s implementation of DomainPolicy as shown in the following listing:

public class CocoonSecurityManager : HostSecurityManager
{
   private static byte[] s_msPublicKey =
   {
      0,   36,   0,   0,   4, 128,   0,   0, 148,   0,   0,   0,   6,   2,
      0,    0,   0,  36,   0,   0,  82,  83,  65,  49,   0,   4,   0,   0,
      1,    0,   1,   0,   7, 209, 250,  87, 196, 174, 217, 240, 163,  46,
      132,  170,  15, 174, 253,  13, 233, 232, 253, 106, 236, 143, 135,
      251,    3, 118, 108, 131,  76, 153, 146,  30, 178,  59, 231, 154,
      217,  213, 220, 193, 221, 154, 210,  54,  19,  33,   2, 144,  11,
      114,   60, 249, 128, 149, 127, 196, 225, 119,  16, 143, 198,   7,
      119,   79,  41, 232,  50,  14, 146, 234,   5, 236, 228, 232,  33,
      192,  165, 239, 232, 241, 100,  92,  76,  12, 147, 193, 171, 153,
      40,   93,  98,  44, 170, 101,  44,  29, 250, 214,  61, 116,  93,
      111,   45, 229, 241, 126,  94, 175,  15, 196, 150,  61,  38,  28,
      138,   18,  67, 101,  24,  32, 109, 192, 147,  52,  77,  90, 210,
      147
   };

   private static byte[] s_ecmaPublicKey =
   {
      0,   0,   0,   0,   0,   0,   0,   0,   4,   0,   0,   0,
      0,   0,   0,   0
   };
   private static byte[] s_CocoonHostRuntimePublicKey =
   {
      0,  36,   0,   0,   4, 128,   0,   0, 148,   0,   0,   0,
      6,   2,   0,   0,   0,  36,   0,   0,  82,  83,  65,  49,
      0,   4,   0,   0,   1,   0,   1,   0, 241, 255, 223,  68,
      103,  53,  57, 194,  68, 246,  41,  44, 219, 236, 159,  34,
      224, 176, 134, 172, 137,  77,  26, 145, 228, 143, 130,  16,
      75,  36, 135,  78, 188, 240,  60, 158, 191,  99, 180,  73,
      195, 154,  43,  24, 231, 230,  59,  49, 123, 233,  45, 148,
      56,   6, 192,  62, 100, 214,  15, 121,   2, 187, 167,  54,
      124,  15, 222,  25, 189, 129, 195, 28,  141, 227, 254, 209,
      189, 241,  48, 114, 192, 210, 132, 218,  80,  70, 248, 240,
      163,  79, 121, 196,  44,  83,  64, 217,  55,  19,  31, 204,
      104, 138,  91,  82, 208,  10,  72, 112, 214,  44, 127,  47,
      186,  72,  80, 101, 227, 240, 184,  27, 181,  50, 137, 147,
      173, 222, 101, 231
   };

   public override PolicyLevel DomainPolicy
   {
      get
      {
         PolicyLevel pol = PolicyLevel.CreateAppDomainLevel();

         pol.RootCodeGroup.PolicyStatement = new PolicyStatement(
            new PermissionSet(PermissionState.None));

         // Create membership condition for the MS platform key.
         UnionCodeGroup msKeyCG =
            new UnionCodeGroup (
               new StrongNameMembershipCondition(new
                  StrongNamePublicKeyBlob(s_msPublicKey),
                  null, null),
                  new PolicyStatement(
                     new PermissionSet(PermissionState.Unrestricted)));

         // Add this code group as a child of the root.
         pol.RootCodeGroup.AddChild(msKeyCG);

         // Create membership condition for the ECMA key.
         UnionCodeGroup ecmaKeyCG =
            new UnionCodeGroup(
               new StrongNameMembershipCondition(new
                  StrongNamePublicKeyBlob(s_ecmaPublicKey), null, null),
                  new PolicyStatement(
                     new PermissionSet(PermissionState.Unrestricted)));

         // Add this code group as a child of the root.
         pol.RootCodeGroup.AddChild(ecmaKeyCG);

         // Create membership condition for the key that
         // signed CocoonHostRuntime.
         UnionCodeGroup hostKeyCG =
            new UnionCodeGroup(
               new StrongNameMembershipCondition(
                   new StrongNamePublicKeyBlob(
                      s_CocoonHostRuntimePublicKey),
                      null, null),
                      new PolicyStatement(
                         new PermissionSet(PermissionState.Unrestricted)));

         // Add this code group as a child of the root.
         pol.RootCodeGroup.AddChild(hostKeyCG);

         // Create the permission set I'll grant to assemblies
         // that satisfy the custom membership condition. Grant the
         // permission to execute, display UI, and access
         // IsolatedStorage.
         PermissionSet pSet = new PermissionSet(PermissionState.None);

         // Add permission to execute.
         pSet.AddPermission(
            new SecurityPermission(SecurityPermissionFlag.Execution));

         // Add permission to display UI.
         pSet.AddPermission(
            new UIPermission(PermissionState.Unrestricted));

         // Add permission to store 10k of data in isolated storage.
         IsolatedStorageFilePermission isoStorePermission =
            new IsolatedStorageFilePermission(PermissionState.None);
            isoStorePermission.UsageAllowed =
               IsolatedStorageContainment.DomainIsolationByUser;
            isoStorePermission.UserQuota = 10000;
            pSet.AddPermission(isoStorePermission);

         // Create a code group with the custom membership
         // condition and grant set.
         UnionCodeGroup cocoonCG =
            new UnionCodeGroup(new CocoonMembershipCondition(),
            new PolicyStatement(pSet));

         // Add this code group as a child of the root.
         pol.RootCodeGroup.AddChild(cocoonCG);

         return pol;
      }
   }
}

Now that I’ve built the policy tree, let’s revisit the initial security requirements to see how they are satisfied. There were two requirements. The assemblies in a cocoon file (1) can reference only other assemblies contained in the cocoon and the .NET Framework assemblies, and (2) will have the permission only to execute, display user interface, and store files in isolated storage. The first requirement is satisfied because the policy tree does not have a code group that grants any permissions to assemblies other than the assemblies in the cocoon, the .NET Framework assemblies, and the CocoonHostRuntime. If an attempt is made to load any other assembly into the application domain, the policy tree will grant it no permissions. Because the final grant set for an assembly is determined by intersecting the permission grants from each of the four policy levels, the fact that I’ve granted no permissions will cause the assembly’s final grant set to be empty. Without at least the permission to execute, the assembly won’t run in the application domain. The second requirement is satisfied by the code group that I specifically built to grant permissions to assemblies in cocoons. That code group grants only the permissions I want. Again, because the overall grant set is determined by intersection, I am guaranteed that no other policy level can grant more permissions than I’d like.

Note

Note

System.AppDomain has a method called SetAppDomainPolicy that can also be used to associate a CAS policy tree with an application domain. SetAppDomainPolicy is now deprecated in favor of using the PolicyLevel property introduced in .NET Framework 2.0. However, if your extensible application must run on versions of the .NET Framework earlier than .NET Framework 2.0, you’ll need to use SetAppDomainPolicy instead.

Step 5: Assign Custom Evidence to Assemblies in the Cocoon

The final step in adding CAS support to runcocoon.exe is to associate the custom evidence type (EvCocoon) with all assemblies that are loaded out of cocoon files. I do this using the ProvideAssemblyEvidence method on the host security manager. Because I returned the value HostSecurityManagerFlags.HostAssemblyEvidence from the Flags property of CocoonHostSecurityManager, the CLR will call ProvideAssemblyEvidence each time an assembly is about to be loaded into an application domain. ProvideAssemblyEvidence takes as input an instance of System.Reflection.Assembly identifying the assembly about to be loaded and an instance of System.Security.Policy.Evidence representing the collection of evidence that has been assigned to the assembly so far. That is, the set of evidence that the CLR has automatically assigned as part of loading the assembly. The implementation of ProvideAssemblyEvidence is free to modify this evidence in any way. The modified evidence is then returned from the method. The definition of ProvideAssemblyEvidence is as follows:

public class HostSecurityManager
{
   // The rest of the class definition omitted...

   public override Evidence ProvideAssemblyEvidence(
      Assembly loadedAssembly,
      Evidence evidence);
}

Evidence has several methods that enable you to manipulate the contents of an evidence collection. One of these methods, called AddHost, is specifically designed to allow extensible applications to add evidence to an existing evidence collection. I use AddHost to add an instance of EvCocoon to the collection of evidence passed to ProvideAssemblyEvidence. I then return the modified collection.

Before I can finish the implementation of ProvideAssemblyEvidence, however, I must consider one more important design point. The implementation of ProvideAssemblyEvidence must add an instance of the EvCocoon evidence only to those assemblies that are loaded from the cocoon. However, given that ProvideAssemblyEvidence will be called for every assembly that is loaded into the application domain, how can I determine which calls to ProvideAssemblyEvidence are for assemblies contained in cocoons? The only thing I have to work with is the instance of the Assembly class passed to ProvideAssemblyEvidence. Given that, I must be able to determine whether an assembly comes from a cocoon through some property or method on Assembly.

The best way to solve this problem requires revisiting how assemblies are loaded by runcocoon.exe. Recall from Chapter 8 that runcocoon.exe uses the CLR hosting interfaces to implement an assembly loading manager whose sole purpose is to load assemblies from the cocoon. It is the assembly loading manager, specifically the implementation of IHostAssemblyStore::ProvideAssembly, that knows when an assembly is being loaded from a cocoon. What I need is some mechanism for ProvideAssembly (in the unmanaged portion of the host) to communicate the fact that an assembly was loaded from a cocoon to the implementation of ProvideAssemblyEvidence (in the managed portion of the host). This mechanism exists in the form of the pContext parameter to ProvideAssembly and the HostContext property on Assembly. Here’s the definition of ProvideAssembly from mscoree.idl:

interface IHostAssemblyStore: IUnknown
{
    HRESULT ProvideAssembly
            (
            [in] AssemblyBindInfo *pBindInfo,
            [out] UINT64          *pAssemblyId,
            [out] UINT64          *pContext,
            [out] IStream        **ppStmAssemblyImage,
            [out] IStream        **ppStmPDB);
    // Rest of the interface definition omitted...
}

To pass context information to managed code, the implementation of ProvideAssembly will set a specific value into *pContext each time an assembly is loaded from a cocoon. This value can then be retrieved in ProvideAssemblyEvidence using the HostContext property on Assembly. Here are the relevant parts of the implementation of ProvideAssembly from Chapter 8:

static const int CocoonAssemblyHostContext = 5;

HRESULT STDMETHODCALLTYPE CCocoonAssemblyStore::ProvideAssembly(
                     AssemblyBindInfo *pBindInfo,
                     UINT64           *pAssemblyId,
                     UINT64           *pContext,
                     IStream          **ppStmAssemblyImage,
                     IStream          **ppStmPDB)
{

   // Portions of the implementation omitted...
   *pContext = 0;

   // Try to load the assembly from the cocoon. If the assembly is
   // contained in the cocoon, S_OK will be returned.
   HRESULT hr = m_pStreamIndex->GetStreamForBindingIdentity(
      pBindInfo->lpPostPolicyIdentity,
      pAssemblyId,
      ppStmAssemblyImage);

   if (SUCCEEDED(hr))
   {
      // Set the host context to indicate this assembly was loaded
      // from a cocoon. This data will be used in the host security manager's
      // implementation of ProvideAssemblyEvidence to associate the custom
      // evidence with the assembly.
      *pContext = CocoonAssemblyHostContext;
   }

    return hr;
}

Now that I can easily determine which assemblies came from cocoons, the implementation of ProvideAssemblyEvidence is straightforward:

public class CocoonSecurityManager : HostSecurityManager
{
   // Portions of the class implementation omitted...
   static int CocoonAssemblyHostContext = 5;

   public override Evidence ProvideAssemblyEvidence(Assembly loadedAssembly,
   Evidence evidence)
   {
      if (loadedAssembly.HostContext == CocoonAssemblyHostContext)
      {
         // Add an instance of the cocoon evidence class to the list of host
         // evidence. This evidence will cause the check in
         // CocoonMembershipCondition to pass during policy evaluation,
         // thereby granting the permissions specified in the app-domain-
         // level policy.
         evidence.AddHost(new EvCocoon());

         // Add evidence that identifies this assembly as coming from
         // the MyComputer zone. Without this evidence, the assembly would
         // get no permissions at the machine level, so the grant based on
         // cocoon evidence would get canceled out. In essence, providing
         // this evidence causes the assembly to get "FullTrust" (through
         // default policy), which I then lock back down through Cocoon
         // evidence.
         evidence.AddHost(new Zone(SecurityZone.MyComputer));
      }

      return evidence;
   }
}

There is one subtlety in the preceding code that I haven’t explained. Notice that in addition to associating EvCocoon evidence with assemblies that come from cocoons, I also associate evidence representing the MyComputer zone. The reason I must do this is as follows. Assemblies loaded from an assembly loading manager (IStream*), such as those contained in cocoons, don’t get much of the default evidence that the CLR typically associates with assemblies as they are loaded. Because the CLR doesn’t know where the contents of an assembly represented by an IStream* originated, it cannot assign evidence describing the URL, zone, and so on, so these assemblies have no evidence that would cause the enterprise, machine, and user policy levels to grant them any permissions whatsoever (through default policy). Given this, the permissions I grant through the application-domain-level CAS policy don’t appear in the final grant set; they are intersected out of the final result because the other policy levels have granted nothing.

I can solve this in a few ways. First, I could modify enterprise, machine, or user policy to grant permissions to assemblies based on EvCocoon evidence. Although this approach would work, it’s cumbersome because it requires the default CAS policy files to be edited on every machine on which I want to run an application contained in a cocoon. The other approach is to assign additional evidence to cocoon assemblies that will cause them to be granted permissions by the enterprise, machine, or user level. That is the approach I’ve taken in the preceding code. By granting evidence representing the MyComputer zone, I cause the assemblies in the cocoon to be granted full trust by the default CAS policy at the machine level. Then, when full trust is intersected with the grants I supply at the application domain policy level, the grants form the final set of permissions granted to the assembly.

At first glance it might seem dangerous to associate evidence that will cause a cocoon assembly to be granted full trust at the machine level. However, because the assembly’s final grant set is determined through an intersection of the grant sets from each level, you can effectively narrow down the grant set from full trust to only those permissions specified at the application domain level.

Assigning Evidence Using the Assembly Loading APIs

Assigning evidence using ProvideAssemblyEvidence is convenient because it enables you to assign evidence to all assemblies from a single location. In this way, you can be sure that no assemblies will be loaded into the domain without you having a chance to customize their evidence. However, you can also assign evidence to an assembly at the time you load it using one of the assembly loading APIs. Recall from Chapter 7 that the assembly loading APIs, such as Assembly.Load and AppDomain.Load, all have a parameter that enables you to specify evidence to associate with the assembly. It’s generally more convenient to assign evidence using a host security manager as described. However, if you have a simple scenario in which you don’t need a host security manager (or application domain manager) for any other reason, or if you have context at the point the assembly is loaded that is required to generate the proper evidence, supplying evidence using one of the assembly loading APIs is a perfectly viable solution.

As you’d expect, the parameter that enables you to supply evidence using one of the assembly loading APIs is of type System.Security.Policy.Evidence as shown in the following definition of Assembly.Load:

static public Assembly Load(AssemblyName assemblyRef,
                            Evidence assemblySecurity)

Example 10-2 creates a new evidence collection, adds an instance of EvCocoon to the collection, and passes it to Assembly.Load.

Example 10-2. Evidenceload.cs

using System;
using System.Reflection;
using System.Security;
using System.Security.Policy;
using System.Collections;

namespace EvidenceDisplay
{
   [Serializable]
   class EvCocoon
   {
   }

   class Program
   {
      static void Main(string[] args)
      {
         Evidence cocoonEvidence = new Evidence();
         cocoonEvidence.AddHost(new EvCocoon());
         cocoonEvidence.AddHost(new Zone(SecurityZone.Intranet));

         // Load an assembly.
         Assembly a = Assembly.Load("Utilities",cocoonEvidence);

         // Assembly a is now loaded and ready to use...
      }
   }
}

The evidence you pass to the assembly loading APIs is merged with the evidence supplied by the CLR in the following ways:

  • All types of evidence you pass that are not also supplied by the CLR are simply added to the evidence collection. Thus, the final collection contains both the evidence you supplied along with that provided by the CLR.

  • If you supply a type of evidence that is also supplied by the CLR, your evidence will supersede the evidence already in the collection. For example, when you load an assembly from the local machine, the CLR will assign it evidence of type System.Security.Policy.Zone representing the MyComputer zone. If you supply zone evidence in your call to the assembly loading APIs, representing Intranet, for example, the final collection of evidence associated with the assembly will have Zone evidence representing Intranet.

Remember, too, that if you are using an assembly loading API that loads an assembly into an application domain other than the one in which you’re currently running, the CLR must be able to serialize your custom evidence type across the application domain boundary. This is why the EvCocoon type in Example 10-2 is marked with the [Serializable] custom attribute.

Putting It All Together

Now that I’ve implemented all the pieces required to incorporate CAS into the runcocoon.exe host, let’s take a step back and see how all these pieces fit together. Figure 10-9 shows the sequence of steps that occur at run time to grant a set of permissions to the assemblies in a cocoon application.

Code Access Security in the runcocoon.exe host

Figure 10-9. Code Access Security in the runcocoon.exe host

The following points explain these steps in greater detail:

  1. An application domain is created in which to run the application contained in the cocoon. When the application domain is created, the CLR accesses the DomainPolicy property on the host security manager (implemented by CocoonHostSecurityManager) to access the CAS policy tree associated with the new domain.

  2. As the application is running, the assembly loading manager gets called to load assemblies from the cocoon.

  3. The implementation of IHostAssemblyStore::ProvideAssembly sets a special value into the host context parameter (*pContext) to indicate that a particular assembly was loaded from the cocoon.

  4. The implementation of ProvideAssemblyEvidence in CocoonHostSecurityManager accesses the HostContext property on the instance of Assembly it is passed to determine whether a given assembly is being loaded from a cocoon. I add an instance of EvCocoon to the evidence collection for all assemblies coming from cocoon files.

  5. As the assembly is loaded, the CLR evaluates CAS policy with that assembly’s evidence at the enterprise, machine, user, and application domain levels. The grant sets from each level are intersected to determine the final set of permissions granted to the assembly.

Associating Evidence with an Application Domain

So far, I’ve described evidence as being associated solely with assemblies. Although this is clearly the most common scenario, it’s also possible to associate evidence with an application domain. Application domain evidence is evaluated by CAS policy and a permission set is granted, just as it is done for assembly evidence. The CLR considers the permission set associated with the application domain while walking the stack to ensure that a particular permission demand is enforced. In essence, the application domain becomes another caller when viewed from the perspective of the CLR’s stack walk. Figure 10-10 shows how the stack walk is affected by application domain evidence. In the figure, the assembly that issues the permission demand and all its descendents on the call stack are checked for the required permission. In addition, the permission set associated with the application domain is checked as well.

Associating evidence with an application domain creates an additional permission set that is evaluated as part of the CAS stack walk.

Figure 10-10. Associating evidence with an application domain creates an additional permission set that is evaluated as part of the CAS stack walk.

Application domain evidence can be used to grant a permission set that will restrict the operations that code running in the domain can perform. To see where this is useful, consider the case of an extensible application that is hosting code downloaded from a particular Web site. Let’s say that such an application has a requirement that no code downloaded from the site will be granted more permissions than would be associated with the site’s URL itself. This requirement can be enforced by associating evidence with the application domain that represents the site from which the code running in the domain came. Even if the downloaded code were granted more permission, because of a particular signature perhaps, the evidence and resulting permission set on the application domain will limit what that code can do.

Application domain evidence typically isn’t used if you’ve already defined a CAS policy tree for your domain. The same restrictions you are enforcing through application domain evidence can also be specified using a policy tree. Application domain evidence is useful in scenarios such as the one described previously when your security requirements are straightforward enough to be expressed with application domain evidence without having to construct an entire policy tree.

Evidence is associated with an application domain using the securityInfo parameter either to AppDomain.CreateDomain or the implementation of CreateDomain provided by your application domain manager. (See Chapter 5 for more information about the role of an application domain manager in extensible applications.) For reference, here’s the definition of CreateDomain from System.AppDomainManager:

public class AppDomainManager : MarshalByRefObject
{
        public virtual AppDomain CreateDomain (string friendlyName,
                                               Evidence securityInfo,
                                               AppDomainSetup appDomainInfo)
        {
        }
}

Example 10-3 shows how to use application domain evidence. This example creates a type called TextWriter in a new application domain. TextWriter has a Write method that writes a text string to the specified file. Write accesses the file using the types from the System.IO namespace, which demand FileIOPermission to protect the file system properly. The application domain in which TextWriter is loaded is created with evidence representing the Internet zone. Permission to access the file system (FileIOPermission) is not granted to the Internet zone through default policy, so this application will fail with a security exception, even if the application is run from the local machine.

Example 10-3. Appdomainevidence.cs

using System;
using System.Security;
using System.Security.Policy;
using Utilities;

namespace AppDomainEvidence
{

   class Program
   {
      static void Main(string[] args)
      {
         // Create evidence representing the Internet zone.
         Evidence internetEvidence = new Evidence();
         internetEvidence.AddHost(new Zone(SecurityZone.Internet));

         // Create a new domain with the "Internet" evidence.
         AppDomain ad = AppDomain.CreateDomain("Custom Evidence Domain",
            internetEvidence);

         // Create an instance of the TextWriter class in the new
         // application domain.
         TextWriter w = (TextWriter) ad.CreateInstanceAndUnwrap(
            "Utilities","Utilities.TextWriter");

         // Call a method on TextWriter that writes text to a file.
         // This method demands FileIOPermission. This operation
         // will fail with a security exception because callers in
         // the Internet zone are not granted FileIOPermission through
         // default CAS policy.
         w.Write("file.txt", "Hello World!");

      }
   }
}

The AllowPartiallyTrustedCallers Attribute

As described, one of the primary goals of CAS is to enable scenarios in which code can be partially trusted. Even so, by default, a given assembly must be fully trusted to call a method in another assembly. Although this might seem counter to the spirit of CAS, the goal of this default is to force the author of an assembly to think explicitly about what is required to expose APIs to partially trusted callers. Specifically, an assembly author should perform a full security audit, including an analysis to determine that permission demands are used appropriately, before exposing APIs to callers with partial trust. In other words, the full trust default requires the author of a class library to take explicit action to enable partially trusted callers. The explicit action that is required is the annotation of the assembly with the System.Security.AllowPartiallyTrustedCallersAttribute custom attribute.

It’s useful for you to have an understanding of the AllowPartiallyTrustedCallers attribute for extensible applications that grant partial trust to the code they host. For example, the assemblies I load out of the cocoon files are partially trusted because I grant only SecurityPermission (to execute), UIPermission, and IsolatedStorageFilePermission so assemblies in cocoon files can call only methods in other assemblies that are annotated with AllowPartiallyTrustedCallers. Any attempt to call a method in an assembly without this attribute results in a SecurityException with the following text:

System.Security.SecurityException: That assembly does not allow partially
trusted callers.

Many of the .NET Framework class libraries are accessible by add-ins with partial trust because they are already annotated with AllowPartiallyTrustedCallers. In addition, you might need to use the AllowPartiallyTrustedCallers attribute yourself if, as part of your extensible application, you provide class libraries that are meant to be used by the add-ins you host. Be sure you consider the security ramifications of exposing your assembly to partially trusted callers before annotating your assembly with AllowPartiallyTrustedCallers. The best place to find up-to-date information about the prerequisites for using AllowPartiallyTrustedCallers can be found on the Microsoft Developer Network Web site (http://www.msdn.microsoft.com).

Summary

The CAS system has several extensibility points that enable it to be customized to meet the security requirements of a range of different environments. Authors of extensible applications can control the set of permissions granted to the assemblies running in their application domains by providing a CAS policy tree that maps an assembly’s evidence to a set of granted permissions. The permissions granted by the extensible application are intersected with those granted at the enterprise, machine, and user policy levels to determine the final set of permissions that control which resources the assembly can access.

The HostSecurityManager class in the System.Security namespace provides the infrastructure needed for extensible applications to customize CAS. By providing a class that derives from HostSecurityManager, authors of extensible applications can supply evidence for assemblies loaded into their application domains and supply application domain CAS policy.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.84.169