Security Through the Lifetime of a Managed Process: Fitting It All Together, Page 2
Deployment-Time Security Issues
After you have finished writing, compiling, and strong name signing your assembly, you must deploy it to the machines on which you want it to run. Traditionally, deploying Windows software has consisted of
- Combining complied code and installation instructions into an installation package
- Copying the package onto the target machines
- Running the package to place the included compiled code in the proper directories on the machine and perform housekeeping tasks such as registry key configuration
While this particular method of deployment is still supported, the .NET Framework also supports over-the-network deployment, dynamic loading of code, and assembly sharing through the Global Assembly Cache. Deployment scenarios and features are described in detail in the .NET Framework SDK documentation in the section titled "Deploying Applications."
The particular method you choose to deploy your application is not a security decision per se, but it may impact the security context in which your application runs. Specifically, the default security policy shipped with the .NET Framework is based on the Internet Explorer Zones model, so the set of permissions granted to your assembly will vary depending on what Zone it is located in when it is loaded into the CLR. For example, assemblies downloaded as part of a Web-based application from a Web server located on your local intranet are likely to be granted fewer permissions than when installed in a directory on a local hard drive.
The primary security decision facing developers and administrators when deploying managed applications is whether to add their assemblies to the Global Assembly Cache (GAC). Assemblies that are present in the GAC are potentially accessible to any other assembly running on the machine, including semitrusted code. For example, an assembly running from a Web server in a semitrusted context can instantiate types located in assemblies in the GAC without having read access to the physical files that make up the assembly. (In particular, the Assembly.Load() static method always probes the GAC for the requested assembly.) For this reason, assemblies that have not been properly secured (either with appropriate security permission or by preventing semitrusted assemblies from binding to the shared assembly) should never be loaded into the GAC.
The .NET Framework includes a number of command-line tools that have security-related functions. The Code Access Security Policy tool, Caspol.exe, is a command-line tool that can be used to modify security policy. The Permissions View tool, Permview.exe, will display assembly-level permission requests and declarative demands contained within a specific assembly. The Strong Name (Sn.exe) and Secutil (Secul.exe) utilities are useful for strong name generation, construction, and extraction. PEVerify is a standalone tool that performs type-safety verification and metadata validation checks on an assembly; it may be used to check that classes and methods within an assembly are type-safe without loading the assembly into the CLR. All of these tools are documented in the .NET Framework SDK documentation in the section titled "Tools and Debugger."
One other tool that, while not directly related to security, is impacted by security operations is the Native Image Generator (Ngen.exe). The Native Image Generator tool (sometimes called the "pre-JITer") creates native code from a managed assembly and caches the native code locally. When an assembly is Ngen'ed (processed by the Native Image Generator), any LinkDemand security checks encountered are evaluated respective to the set of permissions that would be granted to the assembly under the current security policy. If the security policy later changes, the native image may be invalidated. Specifically, if the set of permissions granted to an Ngen'ed assembly under the new policy is not a superset of those granted under the old policy, the native image generated under the old security policy will be invalidated and ignored, and the assembly will be Just-In-Time compiled at runtime. Thus, you may need to re-run the Ngen utility to regenerate native images for your assemblies after making modifications to the security policy that change the set of permissions granted to your assemblies.
Execution-Time Security Issues
Having compiled and deployed your assemblies to a target machine, the next step, of course, is to run your code within the Common Language Runtime. A lot of steps occur "under the covers" when you run your HelloWorld.exe managed executable. In this section, we're going to walk through the process by which managed code contained within an assembly is loaded, evaluated by security policy, Just-In-Time compiled, type-safety verified, and finally allowed to execute. The overall process of developing, deploying, and executing managed code is depicted graphically in Figure 1. We have discussed the Development and Deployment boxes previously and will focus solely on Execution in this section.
FIGURE 1 High-level diagram of the process of developing, deploying, and executing managed code.
The diagram in Figure 1 shows how an individual assembly is loaded and executed within the Runtime, but there is a key initial step that must occur before loading any assemblies. Every managed application is run on top of the Runtime within the context of a host. The host is the trusted piece of code that is responsible for launching the Runtime, specifying the conditions under which the Runtime (and.thus managed code within the Runtime) will execute, and controlling the transition to managed code execution. The .NET Framework includes a shell host launching executables from the command line, a host that plugs into Internet Explorer that allows managed objects to run within a semitrusted browser context, and the ASP.NET host for Web applications. After the host has initialized the Runtime, the assembly containing the entry point for the application must be loaded and then control can be transferred to that entry point to begin executing the application. (In the case of shell-launched C# executables, this entry point is the Main method defined in your application.)
Loading an Assembly
Referring to Figure 1, the first step that occurs when loading an assembly into the Runtime is to locate the desired assembly. Typically, assemblies are located on disk or downloaded over the network, but it is also possible for an assembly to be "loaded" dynamically from a byte array. In any case, after the bytes constituting the assembly are located, they are handed to the Runtime's Assembly Loader. The Assembly Loader parses the contents of the assembly and creates the data structures that represent the contents of the assembly to the Runtime. Control then passes to the Policy Manager.
When the Assembly Loader is asked to resolve a reference to an assembly, the reference may be either a simple reference, consisting of just the "friendly name" of an assembly, or a "strong" reference that uses the cryptographic strong name of the referenced assembly. Strong references only successfully resolve if the target assembly has a cryptographically valid strong name (that is, it was signed with the private key corresponding to the public key in the strong name and has not been tampered with since being signed). For performance reasons, assemblies loaded from the Global Assembly Cache are strong name verified only when they are inserted into the GAC; the Runtime depends on the underlying operating system to keep the contents of the GAC secure.
The Assembly Loader is also responsible for resolving file references within a single assembly. An assembly always consists of at least a single file, but that file can contain references to subordinate files that together constitute a single assembly. Such file references are contained within the "assembly manifest" that is stored in the first file in the assembly (the file that is externally referenced by other assemblies). Every file reference contained within the manifest includes the cryptographic hash of the contents of the referenced file. When a file reference needs to be resolved, the Assembly Loader finds the secondary file, computes its hash value, compares that hash value to the value stored in the manifest, and (assuming the match) loads the subordinate file. If the hash values do not match, the subordinate file has been tampered with after the assembly was linked together and the load of the subordinate file fails.
Resolving Policy for an Assembly
The Policy Manager is a core component of the Runtime security system. Its job is to decide what permissions should be granted, in accordance with the policy specification, to every single assembly loaded by the Runtime. Before any managed code from an assembly is executed, the Policy Manager has determined whether the code should be allowed to run at all and, if it is allowed to run, the set of rights with which it will run. Figure 2 provides a high-level view of the operation of the Policy Manager.
FIGURE 2 High-level diagram of the Policy Manager.
There are three distinct inputs to the Policy Manager:
- The current security policy
- The evidence that is known about the assembly
- The set of permission requests, if any, that were made in assembly-level metadata declarations by the assembly author
The security policy is the driving document; it is the specification that describes in detail what rights are granted to an assembly. The security policy in effect for a particular application domain at any point in time consists of four policy levels:
- Enterprise-wide level
- Machine-wide level
- User-specific level
- An optional, application domainspecific level
Each policy level consists of a tree of code groups and membership conditions, and it is against this tree that the evidence is evaluated. Each policy level is evaluated independently to arrive at a set of permissions that the level would grant to the assembly. The intersection of these level grants creates the maximal set of permissions that can be granted by the Policy Manager.
The second input to the Policy Manager is the evidence, the set of facts that are known about the assembly. There are two types of evidence that are provided to the policy system—implicitly trusted evidence and initially untrusted evidence. Implicitly trusted evidence consists of facts that the Policy Manager assumes to be true either because the Policy Manager computed those facts itself or because the facts were supplied by the trusted host that initialized the Runtime ("host-provided evidence"). (An example of the former type of implicitly trusted evidence is the presence of a cryptographically valid strong name or Authenticode signature. The URI from which an assembly was loaded is an example of the latter type of implicitly trusted evidence.) Initially untrusted evidence consists of facts that were embedded in the assembly ("assembly provided evidence") at development time by the code author; they must be independently verified before being used or believed.
The default security policy that is installed by the .NET Framework never considers initially untrusted evidence, but the facility is present as an extension point. For example, third-party certifications of an assembly might be carried as assembly provided evidence.
The third and final input to the Policy Manager is the set of permission requests made by the assembly. These declarations provide hints to the Policy Manager concerning
- The minimum set of permissions that the assembly must be granted to function properly
- The set of permissions that the assembly would like to be granted but are not strictly necessary for minimal operation
- The set of permissions that the assembly never wants to be granted by the policy system
After computing the maximal set of permissions that can be granted to the assembly in accordance with the policy levels, the Policy Manager can reduce that set based on the contents of the permission requests.
Permission requests never increase the set of permissions granted to an assembly beyond the maximal set determined by the policy levels.
After the Policy Manager has determined the in-effect security policy, the set of applicable evidence, and the set of permission requests, it can compute the actual set of permissions to be associated with the assembly. Recall that permissions are granted by the policy system on an assembly-wide basis; all code within an assembly is granted the same set of rights when that assembly is loaded into an application domain. After the set of granted permissions has been computed by the Policy Manager, that set is associated with the Runtime-internal objects that represent the assembly, and individual classes contained within the assembly can be accessed.
Before leaving the Policy Manager, we should mention that there are two security conditions enforced by the Policy Manager that could cause the load of the assembly to fail at this point and never proceed to the Class Loader. The first condition concerns the assembly's set of minimum permission requests. If an assembly contains any minimum permission requests, these requests must be satisfied by the grant set that is output by the policy system for processing of the assembly to continue. If the minimum request set is not a subset of the resulting grant set, a PolicyException is immediately thrown. The second condition that is checked before proceeding is that the assembly has been granted the right to execute code. "The right to run code" on top of the Runtime is represented by an instance of the SecurityPermission class (specifically, SecurityPermission(SecurityPermissionFlag.Execution)). Under default policy, the Policy Manager will check that the set of permissions granted to an assembly contains at least an instance of this flavor of SecurityPermission. If the right to run code is not granted for some reason, that also generates a PolicyException and no further processing of the assembly occurs.