.NET Security Workshop
Home About Workshops Articles Writing Talks Books Contact

7 Customizing Code Access Security

Like most parts of .NET, code access security is customizable. CAS policy is contained in XML configuration files and these files list the classes that provide the policy. You can customize CAS by providing your own classes and mentioning these in the CAS configuration files. In this part of the workshop you will learn how to extend CAS policy through custom evidence.

7.1 Changing CAS Policy

In this example you will extend code groups and permissions. This means that you will change the policy files. It is easy to corrupt the policy files when you do this. The configuration tool will check that values that you supply, but occasionally it will miss an invalid value and this will render the policy file invalid. Typically, the first time you see this will be when you open the configuration tool and you'll find that the policy you have been editing is marked as read-only:

If you get this situation you'll find that you will not be allowed to view the code groups or permissions below the corrupted policy. The first thing to try is to reset policy through the configuration tool, right click on Runtime Security Policy and select Reset All. You'll be presented with a dialog asking you to confirm your decision. If this is not successful, then you can try the command line tool, caspol, instead.

caspol -rs

I find that caspol is usually more successful at resetting policy than the configuration tool. Of course, once you have reset policy you will have to add your custom values again, but this is a good opportunity to check all of the XML files you use to make sure that you are not adding invalid values.

It is a good idea before creating custom permissions that you save the security policies that you have so that if you mess up your policy files during your development then you can restore your previous settings. A previous page explains how to do this.

7.2 Custom Evidence

Custom evidence is very poorly documented, you'll not find an explanation of how to do it in the MSDN library and even the comprehensive tome .NET Security Framework by LaMacchia et al (Addison Wesley, 2002, ISBN 0-672-32184-X) does not give all of the steps. I will outline how to do it here, however, as you can see later, it does not help you much.

The first thing I need to mention is that assemblies have two types of evidence, host evidence and assembly evidence. The host is ASP.NET, IE or the Windows Shell, or it may be a custom process that you have written to host the runtime. Host evidence is the evidence that have been  mentioned so far, and includes things like Site, URL, Hash etc. Assembly evidence is evidence that can be found in the assembly. This evidence is stored in a managed assembly resource called Security.Evidence. This is just a normal resource, there is nothing special about it. The assembly linker tool, al.exe even has a switch (/evidence) that allows you to add an evidence resource to an assembly. However, you do not need to use this tool, the C# compiler is just as capable.

The assembly evidence is a serialized form of the Evidence object. Evidence is a sealed class that acts as a container for host and assembly evidence. You can use any serializable class as the evidence, and Evidence has a method called AddAssembly to allow you to add the evidence to the Evidence object (it would have been better if this method had been called AddAssemblyEvidence). Once you have created an Evidence object you need to serialize it to a file using the BinaryFormatter object. You can then add this file as a resource to the assembly. Microsoft's documentation recommends that you add the Security.Evidence resource using the assembly linker tool (al.exe). Indeed, this is what LaMacchia et al recommends:

Compile your .NET Framework code to a module or several modules. For the C# and VB compilers, this is done by using the /target:module option.

Combine the module(s) and the serialized evidence into an assembly using the ALink tool (al.exe) from the .NET Framework SDK. ALink has a /evidence option that is used to point to a serialized evidence file.

All the /evidence switch does is merely adds the information in the file as an embedded resource called Security.Evidence, the only unusual aspect of it is that it is a private resource, that is, the resource can only be accessed by this assembly. The downside of using the assembly linker tool in this way is that it requires that the code resides in a separate module (a .netmodule file).

In fact, you do not need to use the assembly linker tool, the C# compiler is adequate. If you use the /res:<file>,Security.Evidence switch (where <file> is the name of the evidence file) then it will add a public embedded resource with the correct name. The fact that this resource is public is immaterial. So here are the steps:

  • Create a custom evidence class that is serializable
  • Create an instance of this class with the serializable data initialized
  • Create an Evidence object and add the custom evidence object to it with AddAssembly
  • Serialize this Evidence object to a file with BinaryFormatter
  • Compile the assembly that you want to have custom assembly evidence using the /res:<file>,Security.Evidence switch

Here is a snippet of code that illustrates how to serialize the evidence:

// file is the name of the output file
// obj is the evidence object

static void SerializeEvidence(string file, object obj)
{
   Evidence ev = new Evidence();
   ev.AddAssembly(obj);
   BinaryFormatter ser = new BinaryFormatter();
   using (Stream stm = File.OpenWrite(file))
   {
      ser.Serialize(stm, ev);
   }
}

The idea is that when the runtime loads your assembly it will gather host and assembly evidence and then use this to determine the permissions that the assembly will be granted. You can get the evidence that an assembly has through the Assembly object:

Assembly a = Assembly.GetExecutingAssembly();
IEnumerator e = a.Evidence.GetEnumerator();
while(e.MoveNext())
{
   Evidence ev = e.Current as Evidence;
   Console.WriteLine("Evidence is {0}", ev.GetType().ToString());
}

This code will print out all the evidence for the assembly. The Evidence class has GetAssemblyEnumerator and GetHostEnumerator to get the assembly and host evidence, respectively. Note that for an assembly to be able to get access to its own evidence the assembly must be granted SecurityPermissionFlag.ControlEvidence. Code that is granted the LocalIntranet and Internet permission sets will not have this permission.

If you peruse the MSDN documentation you'll see that for every evidence type there is also a membership condition type, for example, the Zone type has an associated ZoneMembershipCondition. The membership condition class is used to determine whether a particular evidence has a specific value, and hence indicates that the assembly is a member of a particular code group. For example, this was taken from security.config:

 <CodeGroup class="UnionCodeGroup" version="1" PermissionSetName="Internet" Name="Internet_Zone" Description="...">
   <IMembershipCondition class="ZoneMembershipCondition" version="1" Zone="Internet"/>
   <CodeGroup class="NetCodeGroup" version="1" Name="Internet_Same_Site_Access" Description="...">
      <IMembershipCondition class="AllMembershipCondition" version="1"/>
   </CodeGroup>
</CodeGroup>

As you can see the XML gives the rules that an assembly has to obey to be part of the code group and get the specified permission set. So, for the Internet_Zone a ZoneMembershipCondition object is created and its Zone property is initialized with "Internet". When an assembly is assessed, its evidence is passed to this ZoneMembershipCondition object and the IMembershipCondition.Check method is called. This method iterates through the Evidence object and if there is a Zone evidence object the condition object determines the zone that it refers to and if it is the same as its Zone property (Internet) the Check method will return true.

To try this out, create a file called evidence.cs to contain the classes in this example, and make sure that you have access to a public-private key pair. Initially, the source file looks like this:

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

[assembly: AssemblyKeyFile("key.snk")]
[assembly: AssemblyVersion("1.0.0.0")]

[Serializable]
public class GuidEvidence
{
   Guid guid;
   public GuidEvidence(Guid g)
   {
      guid = g;
   }
   public Guid Guid
   {
      get{return guid;}
   }
   public override string ToString()
   {
      return guid.ToString();
   }
}

The evidence here is a GUID, the class is serializable, so the GUID will be serialized to the serialization stream. Note that this file has a strong name and is versioned. The strong name is important because the assembly must be put in the GAC, the version must not be 0.0.0.0 (the default), because for some reason CAS does not like policy files with this version (although it is a valid version). To use this class as custom evidence you must write a tool to create an instance and then serialize it to a file that you can add as a resource (createEvidence.cs):

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Policy;

class App
{
   // Command line: first parameter is the name of the output file,
   // the second parameter is optional, and is a GUID
   static void Main(string[] args)
   {
      if (args.Length == 0) return;
      if (File.Exists(args[0])) File.Delete(args[0]);

      Guid guid;
      if (args.Length == 2) guid = new Guid(args[1]);
      else
      {
         guid = Guid.NewGuid();
         Console.WriteLine("New GUID {0}", guid.ToString());
      }
      GuidEvidence ge = new GuidEvidence(guid);
      SerializeEvidence(args[0], ge);
   }
   static void SerializeEvidence(string file, object obj)
   {
      Evidence ev = new Evidence();
      ev.AddAssembly(obj);
      BinaryFormatter ser = new BinaryFormatter();
      using (Stream stm = File.OpenWrite(file))
      {
         ser.Serialize(stm, ev);
      }
   }
}

Compile these two assemblies and add the library to the GAC:

csc /t:library evidence.cs
csc createEvidence.cs /r:evidence.dll
gacutil /i evidence.dll

Run createEvidence passing lib.evidence as the parameter, record the GUID that it prints on the command line. Now create a library (lib.cs) that has a strong name provided through another key file lib.snk:

using System;
using System.Collections;
using System.Reflection;

[assembly: AssemblyKeyFile("lib.snk")]

public class LibraryCode
{
   public static string GetData(string str)
   {
      Assembly a = Assembly.GetExecutingAssembly();
      Console.WriteLine("Host evidence:");
      IEnumerator e = a.Evidence.GetHostEnumerator();
      while(e.MoveNext())
      {
         Console.WriteLine(e.Current.ToString());
      }
      Console.WriteLine("Assembly evidence:");
      e = a.Evidence.GetAssemblyEnumerator();
      while(e.MoveNext())
      {
         Console.WriteLine(e.Current.ToString());
      }
      return null;
   }
}

For the time being, this method will not return a value, we'll just use it to dump the evidence that will be used to determine its permissions. For the first test you need a managed process (app.cs):

using System;

class App
{
   static void Main(string[] args)
   {
      if (args.Length > 0)
      {
         Console.WriteLine("{0}", LibraryCode.GetData(args[0]));
      }
   }
}

Compile these files:

csc /t:library lib.cs /res:lib.evidence,Security.Evidence
csc app.cs /r:lib.dll

If you run this process (make sure it has one command line parameter, it does not matter what) you'll see lots of data printed at the command line, here is an edited example:

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

<System.Security.Policy.Url version="1">
   <Url>file:///C:/TestFolder/lib.DLL</Url>
</System.Security.Policy.Url>

<StrongName version="1"
   Key="..."
   Name="lib"
   Version="0.0.0.0"/>

<System.Security.Policy.Hash version="1">
   <RawData>[data removed]</RawData>
</System.Security.Policy.Hash>

Assembly evidence:
dcd89978-2656-4a33-ab79-73423be826c3

The last item is the GuidEvidence we added to the assembly. Now let's get the library to do something. Replace the previous code with the following (also add a using statement for System.IO):

public static string GetData(string str)
{
   StreamReader sr = new StreamReader(str);
   return sr.ReadToEnd();
}

The idea is that the parameter passed to GetData indicates a file and this method will read the entire file and return its contents. This code needs at least a read FileIOPermission, but since the library is installed on this machine it will get full trust. To reduce the permissions granted to this library we need to have it loaded from a different location. Create a configuration file for the process (app.exe.config):

<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="lib"
          publicKeyToken="c7611f8614380ed3"/>
        <codeBase version="0.0.0.0"
          href="http://127.0.0.1/bin/lib.dll"/>
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

(You will need to replace the publicKeyToken used here with the public key token in your library, obtained with sn -Tp). Move the library to the indicated folder (move lib.dll \InetPub\wwwroot\bin). The site used is in dotted format and so the runtime treats this as being in the Internet zone, which means that the assembly will get partial trust.

Now run the application providing the name of a text file, for example, app app.cs. You'll find that a security exception will be thrown because the assembly does not have the following permission:

The state of the failed permission was:
<IPermission class="System.Security.Permissions.FileIOPermission,
  mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1"
  Read="C:\TestFolder\app.cs"/>

Clearly you need to give the assembly the FileIOPermission, and in particular any assembly that has the appropriate evidence should be given this permission. To do this you have to create a membership condition class that will implement IMembershipCondition. When you create a code group you will provide information about the membership condition class and provide a value that, if met, indicates that the assembly with this evidence will have the permissions in the code group. The configuration tool only understands standard evidence and membership condition classes, so to use your custom evidence in a policy you must provide the information through an XML file in this format:

<IMembershipCondition class="fully qualified name of the membership condition class" version="1"
      <!-- one or more attributes with the values that must be met -->
   />
We want to check that the GUID embedded in an assembly is equal to a particular GUID that is placed in the policy, so our evidence assembly will contain a class called GuidMembershipCondition, and the XML will look like this:

<IMembershipCondition class="GuidMembershipCondition, evidence, Version=1.0.0.0,
      Culture=neutral, PublicKeyToken=c7611f8614380ed3" version="1"
   Guid="dcd89978-2656-4a33-ab79-73423be826c3"
/>

Here, you can see that the XML contains the fully qualified name of the class, including its full assembly name. The runtime will create an instance of this class and then initialize the properties of the class that correspond to the attributes in the XML element. In this case, the GuidMembershipCondition class will have a string property called Guid, and the XML file contains the GUID that we added as evidence to the previous assembly.

The membership condition class looks like this (evidence.cs):

public class GuidMembershipCondition : IMembershipCondition
{
   Guid guid;
   public string Guid
   {
      get{return guid.ToString();}
      set{guid = new Guid(value);}
   }
   //IMembershipCondition specific methods
   public bool Check(Evidence ev);
   public IMembershipCondition Copy();
   bool IMembershipCondition.Equals(object ev);
   string IMembershipCondition.ToString();
   // ISecurityEncodable methods
   void ISecurityEncodable.FromXml(SecurityElement e);
   SecurityElement ISecurityEncodable.ToXml();
   // ISecurityPolicyEncodable methods
   void ISecurityPolicyEncodable.FromXml(SecurityElement e, PolicyLevel pl);
   SecurityElement ISecurityPolicyEncodable.ToXml(PolicyLevel pl);
}

The IMembershipCondition interface derives from ISecurityEncodable and ISecurityPolicyEncodable. The last two interfaces have FromXml and ToXml that convert from a SecurityElement object to a GuidMembershipCondition object or from a GuidMembershipCondition object to a SecurityElement object. The SecurityElement class is a lightweight XML object: it does not understand all the nuances of XML, but does understand the concepts of elements and attributes. The difference between these two interfaces is that ISecurityPolicyEncodable will make the conversion for a specific policy. We don't need policy specific information in our implementation so the two interfaces can share the same code. Here's the implementation:

void ISecurityEncodable.FromXml(SecurityElement e)
{
   string g = e.Attribute("Guid");
   if (g != null) guid = new Guid(g);
}
SecurityElement ISecurityEncodable.ToXml()
{
   SecurityElement e = new SecurityElement("IMembershipCondition");
   e.AddAttribute("class", this.GetType().AssemblyQualifiedName);
   e.AddAttribute("version", "1");
   e.AddAttribute("Guid", guid.ToString());
   return e;
}
void ISecurityPolicyEncodable.FromXml(SecurityElement e, PolicyLevel pl)
{
   ISecurityEncodable ise = this as ISecurityEncodable;
   ise.FromXml(e);
}
SecurityElement ISecurityPolicyEncodable.ToXml(PolicyLevel pl)
{
   ISecurityEncodable ise = this as ISecurityEncodable;
   return ise.ToXml();
}

The class has a property called Guid, which will be initialized from the data in the policy when a comparison with evidence will be made. The comparison is performed in the Check method:

public bool Check(Evidence ev)
{
   foreach(object o in ev)
   {
      GuidEvidence g = o as GuidEvidence;
      if (g != null)
      {
         return g.Guid.Equals(guid);
      }
   }
   return false;
}

The method is passed an Evidence object, which as has already been mentioned is a collection of evidence objects obtained from the assembly. This code iterates through the evidence objects and if one of them is a GuidEvidence object with the same value as the condition in the GuidMembershipCondition object (which is obtained from policy) then the method returns true.

The final methods in the class provide a clone of the current condition object, compares two GuidMembershipCondition objects for equality, and returns a string representation:

public IMembershipCondition Copy()
{
   GuidMembershipCondition cond = new GuidMembershipCondition();
   cond.guid = this.guid;
   return cond;
}
bool IMembershipCondition.Equals(object ev)
{
   GuidEvidence g = ev as GuidEvidence;
   if (g != null)
   {
      return g.Guid.Equals(guid);
   }
   return false;
}
string IMembershipCondition.ToString()
{
   return String.Format(
      "<IMembershipCondition class=\"{0}\" version=\"1\" Guid=\"{1}\" />",
      this.GetType().AssemblyQualifiedName,
      guid.ToString());
}

Now you can compile this assembly. It must be added to the GAC so that the configuration tool can access it:

csc /t:library evidence.cs
gacutil /i evidence.dll

Before you can use this new evidence you have to add it as a policy assembly.

.NET Version 3.0
Since all assemblies in the GAC have full trust in .NET version 3.0/2.0 you do not have to add the evidence assembly as a policy assembly. If you do this with the configuration tool, you will get the confusing error: Unable to add the selected assembly. The assembly must have a strong name (name, version and public key). caspol is more exact, it tells you that the action you tried to perform is meaningless.

There are two ways to do this, either use the configuration tool or caspol. An assembly that contains CAS policy classes should not be subject to CAS itself otherwise there would be a possibility that there will be circular references. Thus, the policy assembly should be fully trusted by the system. To do this with caspol you use the following:

caspol -machine -addfulltrust evidence.dll

You have to specify which policy that the assembly will apply to (in this case the Machine policy), or you could specify -enterprise or -user.

To do the same thing with the configuration tool, you need to open the appropriate policy, select Policy Assemblies, right click and select Add. This will show you the following dialog:

You will be shown all the assemblies in the GAC and be able to select the appropriate assembly. Again, this will give the assembly full trust.

It is worth pointing out here that the configuration tool holds onto a copy of the security configuration file. You can only guarantee that it flushes changes to the policy by closing the configuration tool after you have made the policy changes.

Now that you have a new type of evidence you can create a code group appropriate to this evidence. First, create a permission set that will give assemblies access to files. Select Permission Sets, right click and select New. Give this the name GuidPermissions, click on Next and in the following dialog add the File IO permission to the Assigned permissions:

Click on OK and then Finish. Next, you need to add a code group that grants this permission. Open Code Groups and then select All_Code, right click and select New. Fill in the Name with an appropriate name for the code group (say, My_GUID) and click on Next. In the next dialog select (custom) from the drop down list box and you'll see the following:

The XML box is read only, so you have to import an XML file with the correct details. Open Notepad and type the following:

<IMembershipCondition class="GuidMembershipCondition, evidence, Version=1.0.0.0,
   Culture=neutral, PublicKeyToken=c7611f8614380ed3" version="1"
   Guid="dcd89978-2656-4a33-ab79-73423be826c3"
/>

It is important that the value for the class attribute occupies just one line. In the example above I have had to split it over two lines, do not do this. The Guid value is the one that you generated with createEvidence. Note that the PublicKeyToken must correspond to the token in your assembly. If you do not know what it is type the following at the command line:

sn -Tp evidence.dll

Save the XML file (cust_evidence.xml) to a location where you can find it and close Notepad. Move back to the configuration tool, click on Import, navigate to, and select, the XML file you created. You'll either find that the XML is shown on the page, or that you'll get an error box indicating that the XML is invalid. Unfortunately, the error dialog gives no clues about why the XML is invalid, so if you see this dialog you have to check the XML very carefully. Here's some things to check:

  • Is the IMembershipCondition element spelt correctly (check capitalization)
  • Is it terminated correctly? (ie is there a /> at the end)
  • Are attribute values in matching quotes
  • Have you given the fully qualified name for the class, that is, the full name (with namespace) and the full name of the assembly?
  • Is the full name of the class on one line - in the example above I have split it over two lines.

Once the membership condition has been accepted you can click on Next to get the page to select the permission set. Select the permission set you added earlier (GuidPermissions), click on Next and then on Finished.

.NET Version 3.0
The .NET 2.0 configuration tool often creates new code groups with a name prefixed with Copy of, this is benign but you may want to rename the code group to remove this prefix.

Now run your application again. This time you'll see that the library assembly is loaded and it has sufficient permissions to be able to access the file you specify (assuming NTFS gives you access). To further investigate this, try and evaluate the code groups and permissions for the library, so right click on Runtime Security Policy and select Evaluate Assembly and type http://127.0.0.1/bin/lib.dll as the assembly name. You should find that the assembly will be a member of the My_GUID code group under Machine\All_Code and you'll also get the Internet_Zone\Internet_Same_Site_Access permission.

The framework's membership condition classes also implement IIdentityPermissionFactory. This interface is used to provide an identity permission (with the CreateIdentityPermission method) and so code can make identity demands. However, for security reasons the runtime only calls CreateIdentityPermission for system classes, so there is no point implementing this interface on your custom evidence membership condition class. Here's a posting on a public newsgroup by Ivan Medvedev on this subject:

CreateIdentityPermission is not called for custom assembly evidence. This is a security measure, done to prevent malicious code from granting itself identity permissions that may allow it to artificially elevate its trust. CreateIdentityPermission is still invoked for host-supplied custom evidence though. If you need to use this functionality it is relatively easy to create your own host (managed or unmanaged). You can also use Assembly.LoadFrom overload that accepts evidence.

This statement says that custom identity conditions are ignored if you use the standard .NET hosts (ASP.NET, console and GUI apps, and IE). However, if you are willing to write your own host to load the assembly, CAS will call your CreateIdentityPermission method and hence your application can make custom identity demands.

Finally, clean up this example:

  • remove the code group My_GUID and the permission set GuidPermissions or simply reset the policy by right clicking on Machine and selecting Reset (or use caspol -m -rs)
  • remove full trust from evidence by selecting the assembly in the Policy Assemblies node, then right click and select Delete (or use caspol -rf evidence.dll)
  • remove the evidence assembly from the GAC (gacutil -u evidence).

Page Seven Continues Here.

I hope that you enjoy this tutorial and value the knowledge that you will gain from it. I am always pleased to hear from people who use this tutorial (contact me). If you find this tutorial useful then please also email your comments to mvpga@microsoft.com.

Errata

If you see an error on this page, please contact me and I will fix the problem.

Continuing Page Seven

This page is (c) 2007 Richard Grimes, all rights reserved