8.3 Extending the .NET Framework

In 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 Explained

To 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:

Implement IMembershipCondition

Membership condition classes must implement the
System.Security.Policy.IMembershipCondition
interface, which extends both the
System.Security.ISecurityPolicyEncodable and
System.Security.ISecurityEncodeable interfaces. We
summarize the members of each interface in Table 8-9.

Implement default constructor

Membership condition classes must implement a default (empty)
constructor. The runtime uses this constructor when it needs to
create a membership condition object from XML contained in the
security policy files.

Make serializable

Membership condition classes must be
serializable.
Because of their simplicity, application of the
System.SerializableAttribute is usually
sufficient. Those membership condition classes that have more complex
serialization requirements must implement the
System.Runtime.Serialization.ISerializable
interface.

Unless you implement ISerializable,
deserialization of an object does not result in the execution of a
constructor, and therefore you should not depend on constructor logic
to configure or modify the state of the deserialized object.

Table 8-9. Members of the IMembershipCondition, ISecurityPolicyEncodable, and ISecurityEncodeable interfaces

Reconstructs the IMembershipCondition
object's state from a specified
SecurityElement

ToXml

Creates and returns a SecurityElement containing
an XML object model representing the
IMembershipCondition object's
state

ISecurityPolicyEncodable

FromXml

The same as ISecurityEncodable.ToXml but handles
any policy level-specific state

ToXml

The same as ISecurityEncodable.FromXml but
includes any policy level-specific state

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:

Make membership conditions simple. Membership conditions classes have access to the entire
Evidence collection of an assembly or application
domain in the Check method, and therefore it is
possible to create custom membership conditions that test complex
membership criteria spanning multiple items of evidence. You must
consider who will use your membership condition class and design it
appropriately. Creating membership classes that test multiple or
complex sets of data can result in security policy that is difficult
for security administrators and users to configure and understand.

Make membership conditions lightweight. You should consider the amount of memory used by your membership
condition classes as well as avoid lengthy processing tasks, such as
network, file, and database access during membership evaluation.

Make membership conditions noninheritable. It is safest to mark your membership condition classes as
sealed in C# and NotInheritable
in Visual Basic .NET so that malicious code cannot subclass them in
an attempt to change its behavior and subvert CAS, or confuse
security administrators.

Override Object.ToString. The security administration tools use ToString to
display a message that describes the condition and value that a
membership is configured to match against. The use of this
information necessitates that the output be concise. For example,
"Site = *.oreilly.com" or
"Author = Peter."

8.3.2 Defining the AuthorMembershipCondition Class

Following 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 attributesatisfying 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:

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:

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:

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:

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:

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 Assembly

The
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:

8.3.4 Using the AuthorMembershipCondition Membership Condition

The 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 assemblies

As 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:

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 XML

You 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:

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:

8.3.4.3 Configuring security policy

With
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:

Specifies that you want to add a new child code group to the existing
group with the label "All_Code"

-custom Peter.xml FullTrust

Specifies that you are adding a custom membership condition contained
in the file Peter.xml and that the permission
set granted by the code group is the named permission set
"FullTrust"

-name "Peter's code"

Specifies the name of the new code group

-description "Code group grants all code written by Peter full trust"

Specifies a description for the new code group

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 Conditions

The 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":