Managed Hosting API Take 3: the Host SecurityManager

Now that we've examined how to customize the AppDomain creation process, lets go back to our EchoAppDomainManager and look at some other places the CLR lets a managed host customize its behavior.  Looking back at the output of running HelloWorld with the EchoAppDomainManager setup, we see:

D:\>set APPDOMAIN_MANAGER_ASM=AppDomainManagers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=2674e92927c46c9e, processorArchitecture=MSIL
D:\>set APPDOMAIN_MANAGER_TYPE=AppDomainManagers.EchoAppDomainManager

D:\>HelloWorld.exe
AppDomain: DefaultDomain, EchoAppDomainManager::.ctor
AppDomain: DefaultDomain, EchoAppDomainManager::InitializeNewDomain
AppDomain: DefaultDomain, EchoAppDomainManager::HostExecutionContextManager
AppDomain: DefaultDomain, EchoAppDomainManager::HostSecurityManager
In HelloWorld::Main
AppDomainManager: EchoAppDomainManager
Hello World
AppDomain: HelloWorld.exe, EchoAppDomainManager::CreateDomain
AppDomain: HelloWorld.exe, EchoAppDomainManager::HostSecurityManager
AppDomain: Second AppDomain, EchoAppDomainManager::.ctor
AppDomain: Second AppDomain, EchoAppDomainManager::InitializeNewDomain
AppDomain: Second AppDomain, EchoAppDomainManager::HostExecutionContextManager
AppDomain: Second AppDomain, EchoAppDomainManager::HostSecurityManager
AppDomain: Second AppDomain, EchoAppDomainManager::HostExecutionContextManager
AppDomainManager: EchoAppDomainManager
Hello World
HelloWorld.exe

D:\>set APPDOMAIN_MANAGER_ASM=
D:\>set APPDOMAIN_MANAGER_TYPE=

It looks like the CLR is grabbing the HostSecurityManager a few times while running HelloWorld.  Like AppDomainManager, HostSecurityManager is just a base class from which custom HostSecurityManagers can derive.  The base HostSecurityManager provides several virtual properties and methods that work as customization points for the host, in the same way that AppDomainManager's properties and methods allow a host to customize the CLR's behavior.  In Whidbey, the customization points are:

Name Property/Method Use
DetermineApplicationTrust Method Determine the trust of a ClickOnce application
ProvideAppDomainEvidence Method Customize the evidence for the current AppDomain
ProvideAssemblyEvidence Method Customize the evidence for a specified assembly
ResolvePolicy Method Resolve evidence into a PermissionSet
DomainPolicy Property AppDomain policy level

HostSecurityManager also has a Flags property, which lets the CLR know which of the above customization points the HostSecurityManager would like to provide.  As you can see from the above table, the HostSecurityManager gets to participate in almost every aspect of the CAS system, from providing policy via DomainPolicy, to providing evidence that will be resolved against that policy via ProvideAppDomainEvidence and ProvideAssemblyEvidence, to actually performing policy resolution via ResolvePolicy.  Obviously this is a pretty powerful hook into the CLR!

To see what's getting called when, lets create an EchoHostSecurityManager in the same vein as our EchoAppDomainManager.  After creating this class, I'll just modify the HostSecurityManager property of the EchoAppDomainManager to point to our new EchoHostSecurityManager, and rerun the hello world application.  One small note, there were a few bugs in the beta 1 release of HostSecurityManager.  The output that I show here is on a build of Whidbey which has these bugs fixed, so if you run this code on beta 1 you'll see some slightly different output.

Examining HostSecurityManager Calls

As an enhancement to the EchoHostSecurityManager, to demonstrate more clearly what's going on, I've modified it to display the assembly that's being resolved and any evidence provided to it when its asked to resolve policy.  This makes the output from the HelloWorld application is much more verbose, as would be expected.  I'll split it up into two portions, one for each AppDomain.  Control starts in the default domain:

AppDomain: DefaultDomain, EchoAppDomainManager::.ctor
AppDomain: DefaultDomain, EchoAppDomainManager::InitializeNewDomain
AppDomain: DefaultDomain, EchoAppDomainManager::HostExecutionContextManager
AppDomain: DefaultDomain, EchoAppDomainManager::HostSecurityManager
AppDomain: DefaultDomain, EchoHostSecurityManager::Flags
AppDomain: DefaultDomain, EchoHostSecurityManager::DomainPolicy
AppDomain: DefaultDomain, EchoHostSecurityManager::Flags
AppDomain: DefaultDomain, EchoAppDomainManager::HostSecurityManager
AppDomain: DefaultDomain, EchoHostSecurityManager::Flags
AppDomain: DefaultDomain, EchoHostSecurityManager::ResolvePolicy
Zone : MyComputer
Url : file:///D:\blog\AppDomainManager\HelloWorld.exe
PermissionRequestEvidence : <System.Security.Policy.PermissionRequestEvidence version="1"/>

AppDomain: DefaultDomain, EchoAppDomainManager::HostSecurityManager
AppDomain: DefaultDomain, EchoHostSecurityManager::Flags
AppDomain: DefaultDomain, EchoHostSecurityManager::ProvideAssemblyEvidence for HelloWorld, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
AppDomain: DefaultDomain, EchoAppDomainManager::HostSecurityManager
AppDomain: DefaultDomain, EchoHostSecurityManager::Flags
AppDomain: DefaultDomain, EchoHostSecurityManager::ResolvePolicy
Zone : MyComputer
Url : file:///D:/blog/AppDomainManager/HelloWorld.exe
Hash : SHA1 a92f107dd3c6b1ce6bafcaee90d90d2c72478c07
PermissionRequestEvidence : <System.Security.Policy.PermissionRequestEvidence version="1"/>

In HelloWorld::Main
AppDomainManager: EchoAppDomainManager
Hello World
AppDomain: HelloWorld.exe, EchoAppDomainManager::CreateDomain
AppDomain: HelloWorld.exe, EchoAppDomainManager::HostSecurityManager
AppDomain: HelloWorld.exe, EchoHostSecurityManager::Flags
AppDomain: HelloWorld.exe, EchoHostSecurityManager::ProvideAssemblyEvidence for HelloWorld, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null

Following along in the output, we see that the first call to the HostSecurityManager is to get the domain policy for the default AppDomain.  (Actually the first call is to the Flags property ... every time the CLR is making this call, its checking to see if the current HostSecurityManager wants to participate in the following call or not.  Since our HostSecurityManager specifies that it wants to participate in all calls, every call to Flags will be followed by a call to the method that the corresponding flag is for).

Next, is a call to ResolvePolicy, which is attempting to resolve the policy on the HelloWorld.exe file.  This all seems ok so far, but the next two calls might seem a little strange given that we've already resolved the policy for HelloWorld.exe.  Why are we being asked to provide evidence for HelloWorld and then resolve the policy again?  It turns out that the first call to ResolvePolicy is done by the loader, in order to ensure that the assembly meets the minimum requirements to even load.  The reason that there's no call to ProvideAssemblyEvidence for this first ResolvePolicy is that there isn't yet an Assembly object to represent HelloWorld.exe, since it hasn't been loaded.  Once we've loaded HelloWorld.exe, now we have a chance to provide additional evidence via the next call to ProvideAssemblyEvidence.  Then we need to resolve the policy again, since any extra evidence we assign wouldn't be useful unless the policy was re-evaluated.

OK, so that all makes sense, but what's with the next call to ProvideAssemblyEvidence, after HelloWorld tries to create the new domain?  From the output, that call is still occurring in the default domain, where we've already resolved the evidence on HelloWorld.exe.  This call is because in HelloWorld we call CreateDomain, but don't pass any evidence in.  In that case, CreateDomainHelper is going to fall back on a set of default evidence for the entry assembly of the current domain.  That means that this call to ProvideAssemblyEvidence is actually a prequel to the upcoming call to ProvideAppDomainEvidence.  In fact, you'll see that any evidence you return from this method will be the default set of evidence for the next ProvideAppDomainEvidence call.

The Second AppDomain

In the second AppDomain, the sequence of calls should make sense:

AppDomain: Domain 2, EchoAppDomainManager::.ctor
AppDomain: Domain 2, EchoAppDomainManager::InitializeNewDomain
AppDomain: Domain 2, EchoAppDomainManager::HostExecutionContextManager
AppDomain: Domain 2, EchoAppDomainManager::HostSecurityManager
AppDomain: Domain 2, EchoHostSecurityManager::Flags
AppDomain: Domain 2, EchoHostSecurityManager::DomainPolicy
AppDomain: Domain 2, EchoHostSecurityManager::Flags
AppDomain: Domain 2, EchoAppDomainManager::HostSecurityManager
AppDomain: Domain 2, EchoHostSecurityManager::Flags
AppDomain: Domain 2, EchoHostSecurityManager::ProvideAppDomainEvidence
AppDomain: Second AppDomain, EchoAppDomainManager::HostSecurityManager
AppDomain: Second AppDomain, EchoHostSecurityManager::Flags
AppDomain: Second AppDomain, EchoHostSecurityManager::ProvideAssemblyEvidence for HelloWorld, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
AppDomain: Second AppDomain, EchoAppDomainManager::HostSecurityManager
AppDomain: Second AppDomain, EchoHostSecurityManager::Flags
AppDomain: Second AppDomain, EchoHostSecurityManager::ResolvePolicy
Zone : MyComputer
Url : file:///D:/blog/AppDomainManager/HelloWorld.EXE
Hash : SHA1 a92f107dd3c6b1ce6bafcaee90d90d2c72478c07
PermissionRequestEvidence : <System.Security.Policy.PermissionRequestEvidence version="1"/>

AppDomain: Second AppDomain, EchoAppDomainManager::HostExecutionContextManager
AppDomainManager: EchoAppDomainManager
Hello World

First we set the AppDomain policy, then we ProvideAppDomainEvidence (a call that couldn't be done in the default domain, since a domain did not yet exist to load the HostSecurityManager in).  That sets up the security context of the AppDomain, so now we can begin to load assemblies into it.  So, the next call to HostSecurityManager is to provide the evidence for HelloWorld.exe, and then resolve the policy on that assembly.

Wrapping Up

A couple of things to note here.  The PermissionRequestEvidence you see bubbling through the calls to ResolvePolicy are actually a nicely wrapped up version of the assembly's declarative security.  Each PermissionRequestEvidence object contains a property for Requested, Optional, and Denied permission sets.  You can use this information to help you figure out what type of policy to apply.

How would you ever use this feature?  As you can imagine there are lots of uses.  One scenario I can think of would be for an application that wants to grant FullTrust to every assembly that's part of the application.  However, this application might support third party plugins.  If these plugins will require FullTrust, then the HostSecurityManager might look like this:

public class PluginApplicationHostSecurityManager : HostSecurityManager
{
    public override Evidence ProvideAssemblyEvidence(Assembly loadedAssembly, Evidence inputEvidence)
    {
        // add PluginApplicationAssemblyEvidence if the assembly is a part of the application
        if(IsSignedWithAppKey(loadedAssembly) || IsAppPlugin(loadedAssembly))
            inputEvidence.AddHost(new PluginApplicationAssemblyEvidence());

        return base.ProvideAssemblyEvidence(loadedAssembly, inputEvidence);
    }

    public override PermissionSet ResolvePolicy(Evidence evidence)
    {
        IEnumerator hostEvidence = evidence.GetHostEnumerator();
        while(hostEvidence.MoveNext())
        {
            // allow FullTrust to any plugin that is part of the application
            if(hostEvidence.Current is PluginApplicationAssemblyEvidence)
                return new PermissionSet(PermissionState.Unrestricted);
        }

        return base.ResolvePolicy(evidence);
    }

    private bool IsSignedWithAppKey(Assembly inputAssembly)
    {
        // ...
    }

    private bool IsAppPlugin(Assembly inputAssembly)
    {
        // ...
    }
}