[ Team LiB ] |
8.3 Extending the .NET FrameworkIn Section 6.3.1.1, we developed the Author evidence class, which identifies the developer of an assembly. In this section, we extend CAS so that you can base policy resolution on the presence of Author evidence. 8.3.1 Custom Membership Conditions ExplainedTo enable Author evidence to drive policy resolution, first you create a custom membership condition class. Using the techniques described in Section 8.2, you can then use the custom membership condition in code groups to test the values of any Author evidence presented by an assembly or application domain. Here are the key requirements of a custom membership condition class:
In addition to these requirements, many of the guidelines we outlined for evidence in Chapter 6 also hold true for custom membership conditions. These include:
8.3.2 Defining the AuthorMembershipCondition ClassFollowing the naming pattern used in the .NET class library, we name the membership condition class AuthorMembershipCondition. The AuthorMembershipCondition class is relatively simple; however, there is still a fair amount of code involved because we have to implement the eight members defined in the IMembershipCondition interface. We will break the AuthorMembershipCondition class into manageable sections for explanation. The class declaration for AuthorMembershipCondition specifies that it is sealed (C#) and NotInheritable (Visual Basic .NET) to stop malicious code from subclassing it in order to subvert CAS. You should also specify the implementation of IMembershipCondition and annotate AuthorMembershipCondition with the System.Serializable attribute—satisfying the two major requirements of membership condition classes. We define a single private System.String data member named AuthorName, which will contain the name of the author on which an AuthorMembershipCondition object will base its membership test: # C# using System; using System.Security; using System.Collections; using System.Reflection; using System.Security.Policy; [assembly:AssemblyKeyFile("Keys.snk")] [assembly:AssemblyVersion("1.0.0.0")] namespace ORA.DotNetSecurity.Policy { [Serializable] public sealed class AuthorMembershipCondition : IMembershipCondition { private string AuthorName = ""; # Visual Basic .NET Imports System Imports System.Security Imports System.Collections Imports System.Reflection Imports System.Security.Policy <assembly:AssemblyKeyFile("Keys.snk")> <assembly:AssemblyVersion("1.0.0.0")> Namespace ORA.DotNetSecurity.Policy <Serializable> _ Public NotInheritable Class AuthorMembershipCondition Implements IMembershipCondition Private AuthorName As String = "" We define two constructors for AuthorMembershipCondition. The first is a default (empty) constructor. The runtime uses this constructor to create AuthorMembershipCondition objects before calling the FromXml method to recreate the state of an AuthorMembershipCondition from XML stored in the policy configuration files. The second constructor is for general use and takes a System.String argument containing the name of the author on which the AuthorMembershipCondition should base its membership test: # C# public AuthorMembershipCondition( ) { this.AuthorName = ""; } public AuthorMembershipCondition(string author) { if (VerifyAuthorName(author)) { this.AuthorName = author; } } # Visual Basic.NET Public Sub New( ) Me.AuthorName = "" End Sub Public Sub New(ByVal author As String) If VerifyAuthorName(author) Then Me.AuthorName = author End If End Sub We include a Name property to provide controlled access to the private AuthorName data member and the private VerifyAuthorName utility method, which we use in both the Name property and class constructor to validate the author name provided. To simplify AuthorMembershipCondition you check only if the author name is null (C#) or Nothing (Visual Basic .NET)—in which case, you throw a System.ArgumentNullException. In a production-quality membership condition class, it is good practice to verify the condition data more thoroughly and throw a System.ArgumentException if the data is invalid. For example, it would be a good idea to check for invalid characters and limit the size of the name provided: # C# // Property to get/set the author name public string Name { get { return AuthorName; } set { if (VerifyAuthorName(value)) { this.AuthorName = value; } } } // Utility method to verify that the author name // is not null. private bool VerifyAuthorName(string author) { if (author == null) { throw new ArgumentNullException("author"); } else { return true; } } # Visual Basic .NET ' Property to get/set the author name Public Property Name( ) As String Get Return AuthorName End Get Set (ByVal Value As String) If VerifyAuthorName(Value) Then Me.AuthorName = Value End If End Set End Property ' Utility method to verify that the author name ' is not null. Private Function VerifyAuthorName(ByVal author As String) _ As Boolean If author Is Nothing Then Throw New ArgumentNullException("author") Else Return True End If End Function The real work of the AuthorMembershipCondition class takes place in the Check method. The Check method takes an Evidence collection and enumerates the contained evidence objects to determine if any of them are Author objects. If Check finds an Author object, it compares the Author.Name property with its own private AuthorName data member. If the two names match, Check returns true; otherwise, Check returns false. For simplicity, we have implemented a straightforward case-sensitive string comparison, but you might also consider support for non-case-sensitive comparisons and wildcard matches. The membership condition logic can be arbitrarily complex, but remember that the runtime may call Check many times during policy resolution, and time-consuming membership condition evaluations will affect application performance: # C# // Determines whether the Evidence meets the membership // condition. public bool Check( Evidence evidence ) { // Return false if the Evidence is null if (evidence == null) { return false; } Author auth = null; // Enumerate across the host and assembly evidence // collections. IEnumerator enumerator = evidence.GetEnumerator( ); while (enumerator.MoveNext( )) { auth = enumerator.Current as Author; // If the evidence is of type Author and the // author names match, the evidence meets the // membership condition. if (auth != null) { if (auth.Name == this.AuthorName) { return true; } } } return false; } # Visual Basic .NET ' Determines whether the Evidence meets the membership ' condition. Public Function Check(ByVal evidence As Evidence) As Boolean _ Implements IMembershipCondition.Check ' Return false if the Evidence is Nothing If evidence Is Nothing Then Return False End If Dim auth As Author = Nothing ' Enumerate across the host and assembly evidence ' collections. Dim enumerator As IEnumerator = evidence.GetEnumerator( ) While enumerator.MoveNext( ) auth = CType(enumerator.Current, Author) ' If the evidence is of type Author and the ' author names match, the evidence meets the ' membership condition. If Not auth Is Nothing Then If auth.Name = Me.AuthorName Then Return True End If End If End While Return False End Function The remaining members of the IMembershipCondition interface are straightforward. The Copy method returns a clone of the current AuthorMembershipCondition object, and the Equals method compares two AuthorMembershipCondition objects for equality based on the value of their private AuthorName data members instead of comparing object references. Because Object also defines an Equals method, you must override it with the one defined in the IMembershipCondition interface. As a result, you must also override the GetHashCode method: # C# // Creates a copy of the membership condition public IMembershipCondition Copy( ) { return new AuthorMembershipCondition(this.AuthorName); } // Compares an object for equality based on the author's // name, not the object reference. public override bool Equals(object obj) { AuthorMembershipCondition that = (obj as AuthorMembershipCondition); if (that != null) { if (this.AuthorName == that.AuthorName) { return true; } } return false; } // We must override GetHashCode because we override // Object.Equals. Returns a hash code based on the // author's name. public override int GetHashCode( ) { return this.AuthorName.GetHashCode( ); } # Visual Basic .NET ' Creates a copy of the membership condition Public Function Copy( ) As IMembershipCondition _ Implements IMembershipCondition.Copy Return New AuthorMembershipCondition(Me.AuthorName) End Function ' Compares an object for equality based on the author's ' name, not the object reference. Public Overloads Function Equals(ByVal obj As Object) As Boolean _ Implements IMembershipCondition.Equals Dim that As AuthorMembershipCondition = _ CType(obj,AuthorMembershipCondition) If Not that Is Nothing Then If Me.AuthorName = that.AuthorName Then Return True End If End If Return False End Function ' We must override GetHashCode because we override ' Object.Equals. Returns a hash code based on the ' author's name. Public Overrides Function GetHashCode( ) As Integer Return Me.AuthorName.GetHashCode( ) End Function All of the standard membership condition classes override Object.ToString to return a simple human-readable representation of the membership condition they define. The administration tools display this information, and it should be relatively concise. For consistency, we take the same approach: # C# // Returns a simple string representation of the // membership condition public override string ToString( ) { return "Author - " + AuthorName; } # Visual Basic .NET ' Returns a simple string representation of the ' membership condition Public Overrides Function ToString( ) As String _ Implements IMembershipCondition.ToString Return "Author - " & AuthorName End Function Both the ISecurityEncodable and ISecurityPolicyEncodable interfaces define a ToXml method that returns a System.Security.SecurityElement containing an XML object model of the AuthorMembershipCondition object. The AuthorMembershipCondition class does not differentiate between different policy levels and therefore implements both methods the same. Most importantly, the root XML element must be "IMembershipCondition" or the administrative tools will not be able to import the XML describing the custom membership condition. The XML representation of all standard membership condition classes includes a version attribute that represents the XML format used to represent the membership condition. We have taken the same approach, which gives flexibility should future versions of the AuthorMembershipCondition class require a different structure: # C# // Return a SecurityElement containing an XML object // model representing the membership condition. public SecurityElement ToXml( ) { return this.ToXml(null); } // Return a SecurityElement containing an XML object // model representing the membership condition. We have // no need to differentiate between policy levels and so // we ignore the "level" argument. public SecurityElement ToXml(PolicyLevel level) { // Create a new "IMembershipCondition" element SecurityElement se = new SecurityElement("IMembershipCondition"); // Add fully qualified type name for membership condition se.AddAttribute( "class", this.GetType( ).AssemblyQualifiedName ); // Add an XML version number of "1" se.AddAttribute("version", "1"); // Add the author name se.AddAttribute("name", AuthorName); // Return the new SecurityElement return se; } # Visual Basic .NET ' Return a SecurityElement containing an XML object ' model representing the membership condition. Public Function ToXml( ) As SecurityElement _ Implements ISecurityEncodable.ToXml Return Me.ToXml(Nothing) End Function ' Return a SecurityElement containing an XML object ' model representing the membership condition. We have ' no need to differentiate between policy levels and so ' we ignore the "level" argument. Public Function ToXml(ByVal level As PolicyLevel) _ As SecurityElement Implements ISecurityPolicyEncodable.ToXml ' Create a new "IMembershipCondition" element Dim se As SecurityElement = _ New SecurityElement("IMembershipCondition") ' Add fully qualified type name for membership condition se.AddAttribute( _ "class", _ Me.GetType( ).AssemblyQualifiedName _ ) ' Add an XML version number of "1" se.AddAttribute("version", "1") ' Add the author name se.AddAttribute("name", AuthorName) ' Return the new SecurityElement Return se End Function The reverse of the ToXml method is FromXml, which takes a SecuirtyElement and reconstructs the object state. As with ToXml, both the ISecurityEncodable and ISecurityPolicyEncodable interfaces define a FromXml method. Again, you implement both methods the same, because you do not need to perform policy level-specific state configuration: # C# // Reconstruct the state of the membership condition // object from the SecurityElement provided. // fromExtract state from a SecurityElement public void FromXml(SecurityElement e) { this.FromXml(e, null); } // Reconstruct the state of the membership condition // object from the SecurityElement provided. We have // no need to differentiate between policy levels and so // we ignore the "level" argument. public void FromXml(SecurityElement e, PolicyLevel level) { // Ensure we have a SecurityElement to work with if (e == null) throw new ArgumentNullException("e"); // Ensure the SecurityElement is an AuthorMembershipCondition if (e.Tag != "IMembershipCondition") { throw new ArgumentException ("Element must be IMembershipCondition"); } else { // Extract the author name from the SecurityElement this.AuthorName = e.Attribute("name"); } } } } # Visual Basic .NET ' Reconstruct the state of the membership condition ' object from the SecurityElement provided. ' fromExtract state from a SecurityElement Public Sub FromXml(ByVal e As SecurityElement) _ Implements ISecurityEncodable.FromXml Me.FromXml(e, Nothing) End Sub ' Reconstruct the state of the membership condition ' object from the SecurityElement provided. We have ' no need to differentiate between policy levels and so ' we ignore the "level" argument. Public Sub FromXml(ByVal e As SecurityElement, _ ByVal level As PolicyLevel) _ Implements ISecurityPolicyEncodable.FromXml ' Ensure we have a SecurityElement to work with If e Is Nothing Then Throw New ArgumentNullException("e") End If ' Ensure the SecurityElement is an AuthorMembershipCondition If e.Tag <> "IMembershipCondition" Then Throw New ArgumentException _ ("Element must be IMembershipCondition") Else ' Extract the author name from the SecurityElement Me.AuthorName = e.Attribute("name") End If End Sub End Class End Namespace 8.3.3 Building the AuthorMembershipCondition AssemblyThe AuthorMembershipCondition class has a dependency on the Author class you created in the Chapter 7. For convenience, you could build all of the Author-related security classes into a single assembly. This would simplify security administration if you needed to distribute CAS extensions to third parties, but because the Author class is evidence, you would need to add the assembly to the fully trusted assembly list of every policy level. This would grant the assembly full trust at runtime, which many people would find unacceptable. For the purpose of an example, build AuthorMembershipCondition into its own assembly using the following command. Remember to ensure that you place a copy of the Author.dll assembly from Chapter 7 in the directory where you build AuthorMembershipCondition: # C# csc /t:library /reference:Author.dll AuthorMembershipCondition.cs # Visual Basic .NET vbc /t:library /reference:Author.dll AuthorMembershipCondition.vb 8.3.4 Using the AuthorMembershipCondition Membership ConditionThe Microsoft .NET Framework Configuration tool (Mscorcfg.msc) provides a graphical interface through which you can configure security policy. Mscorcfg.msc implements specific support for the standard membership condition classes and provides user-friendly interfaces through which you can configure their membership condition parameters. However, the support for using custom membership conditions is rudimentary, requiring you to import an XML fragment describing the membership condition to use. This XML is the result of calling ToXml( ).ToString( ) on a custom membership condition object. In this example, we will use the Code Access Security Policy tool (Caspol.exe) to configure a code group in the user policy level that uses AuthorMembershipCondition to evaluate membership. We will describe the Caspol.exe commands necessary to perform this configuration, but you should look at Chapter 9 for complete details of both the command-line and graphical administration tools. 8.3.4.1 Installing security assembliesAs we discussed in Section 8.1.1.3, assemblies that provide CAS extensions (such as the AuthorMembershipCondition.dll assembly) must be fully trusted by the policy level in which they are used. To make AuthorMembershipCondition.dll fully trusted by the user policy level, execute the following commands from the directory where the AuthorMembershipCondition is located: gacutil -i AuthorMembershipCondition.dll caspol -user -addfulltrust AuthorMembershipCondition.dll The first command installs AuthorMembershipCondition into the global assembly cache (which you must do before you can make it a fully trusted assembly). The second command makes AuthorMembershipCondition a fully trusted assembly in the user policy level. 8.3.4.2 Generating AuthorMembershipCondition XMLYou must create an XML representation of the membership condition you want to assign to the code group. Although the required XML is simple enough that you could create it manually, it is much easier and safer to instantiate an AuthorMembershipCondition object and write the contents of the SecurityElement returned by the ToXml to disk. If you distribute custom membership condition classes to users or customers, we advise you to create a simple utility to perform the creation of the required XML fragments. The CreateAuthorMembership class takes an author name as a command-line argument, instantiates an AuthorMembershipCondition object, and writes an XML representation of it to disk: # C# using System; using System.IO; using ORA.DotNetSecurity.Policy; public class CreateAuthorMembership { public static void Main(string[] args) { // Create a new AuthorMembershipCondition based // on the name of the author provided AuthorMembershipCondition amc = new AuthorMembershipCondition(args[0]); // Generate the name of the XML output file String file = args[0] + ".xml"; // Render the AuthorMembershipCondition to // XML and write it to a file StreamWriter strm = new StreamWriter(file); strm.Write(amc.ToXml( ).ToString( )); strm.Close( ); // Display result Console.WriteLine("Created author membership condition : " + file); } } # Visual Basic .NET Imports System Imports System.IO Imports ORA.DotNetSecurity.Policy Public Class CreateAuthorMembership Public Shared Sub Main(ByVal args( ) As String) ' Create a new AuthorMembershipCondition based ' on the name of the author provided Dim amc As AuthorMembershipCondition = _ New AuthorMembershipCondition(args(0)) ' Generate the name of the XML output file Dim file As String = args(0) + ".xml" ' Render the AuthorMembershipCondition to ' XML and write it to a file Dim strm As StreamWriter = New StreamWriter(file) strm.Write(amc.ToXml( ).ToString( )) strm.Close( ) ' Display result Console.WriteLine("Created author membership condition : " & _ file) End Sub End Class Build the CreateAuthorMembership utility using the following command, remembering the dependency on the AuthorMembershipCondition.dll library: # C# csc /reference:AuthorMembershipCondition.dll CreateAuthorMembership.cs # Visual Basic .NET vbc /reference:AuthorMembershipCondition.dll CreateAuthorMembership.vb Run the command CreateAuthorMembership Peter to create the XML representation of an AuthorMembershipCondition that matches Author evidence for the author "Peter." Here is the content of the Peter.xml file. The PublicKeyToken value will change based on the keys you used to give create a strong name: <IMembershipCondition class="ORA.DotNetSecurity.Policy. AuthorMembershipCondition, AuthorMembershipCondition, Version=1.0.0.0, Culture=neutral, PublicKeyToken=cc5e18bc387194b3" version="1" name="Peter"/> 8.3.4.3 Configuring security policyWith the Author.dll assembly configured as a fully trusted assembly in the user policy level, you are ready to create a code group whose membership condition is based on Author evidence. Type the following command: caspol -user -addgroup All_Code -custom Peter.xml FullTrust -name "Peter's code" -description "Code group grants all code written by Peter full trust" Here is a summary of the arguments used in this command:
Depending on how Caspol.exe is configured, it may prompt you to confirm the change your are about to make with the following message: The operation you are performing will alter security policy. Are you sure you want to perform this operation? (yes/no) Entering "yes" updates the user security policy and commits the changes to disk; you will see the following message: Added union code group with "-custom" membership condition to the User level. Success 8.3.5 Testing Custom Membership ConditionsThe easiest way to test the extensions to the policy resolution process is to use the Caspol.exe tool. In Chapter 6, we embedded Author evidence in an assembly named HelloWorld.exe. We can use Caspol.exe and the HelloWorld.exe assembly to show that the "Peter's code" code group does detect Author evidence and evaluate it correctly. Type the following command from the directory where HelloWorld.exe is located: caspol -user -resolvegroup HelloWorld.exe Caspol.exe produces the following output, showing that HelloWorld.exe is a member of the code group "Peter's code": Microsoft (R) .NET Framework CasPol 1.0.3705.288 Copyright (C) Microsoft Corporation 1998-2001. All rights reserved. Level = User Code Groups: 1. All code: FullTrust 1.1. Author - Peter: FullTrust Success |
[ Team LiB ] |