Introduction
A software developer may find it necessary to "sell" some of his/her valuable mental skills and labor directly or indirectly to someone in order to make a physical and resulting mental living. The difference is in the "someone", be it his/her boss, a movement/cause or middle/end customers. The term "selling" used here does not necessary result in a cash return, it is used in a more general sense. For example, in open source community, a developer "sells" his/her contribution and gets the benefit of resulting joy of mental achievement and a sense of belonging, the spirit of sharing with and help others, the right to improve or use others contribution, and some other technical advantages of such a mode of development. There is nothing in this article that is explicitly or implicitly against it, it is view in an orthogonal perspective. The technology developed at CryptoGateway that is capable of making the "selling" (in the above sense) to "someone" more manageable by right owners, e.g. the service providing sector of a open source community that owns a public license, and the "someone".
This article is on an access control technology for .NET (and possibly Java) assemblies and the supporting framework. It is an architectural overview, so it does not contain a complete program that can be used immediately. The framework proposed is general. It does not depend on the access control technology that requires it. Since the author is not familiar with Java programming, the rest of the article will use the .NET term for a particular concept.
An assembly in frameworks like .NET contain extensive self-reflective meta information about the type hierarchy contained in the assembly, which can be queried using the .NET framework itself. These information are a great language feature for designing, developing, manipulating (e.g. turning an operation into data of the meta system), and maintaining codes. It is not a welcome feature when the inner details of at least some assemblies in a software system are not intended to be exposed or publicized. Public or authorized sharing of code normally happen at the source code level, assemblies are not usually used to that end. This is because tools exist (or can be developed at low cost) that can be used to reverse-engineer the assemblies given the complete information. It is true that the current trend of software industry is shifting towards more of a service oriented type with the service provider holding most of the important assemblies on their servers. There is still a need to ship some of the assemblies to end users which are likely to contain assemblies that:
- The author does not want it be probed and/or manipulated, e.g. due to legal liability reasons.
- The customer needs to be assured that these assemblies do not contain undeclared functionalities by holding the author responsible for unwanted functionalities, like back-doors, trojan horses, etc.
One way of trying to achieve assembly protection is the use of code obfuscation. The other one, among others, is assembly encryption (with a digital signature as an additional option). The pros and cons of these methods will not be discussed here. If needed, these two techniques can be combined, especially when a non-intrusive encryption method, like the p2p data access control technology of us (see, e.g., 1, 2, 3, and the media player briefly discussed in this article), is employed to realize layered defence.
The Framework
A. The Host
The framework that is considered to be good in general and is flexible enough to support the code protection technology and the resulting development culture should have, or is capable of being evolved into one that has the following properties:
- There is a host assembly that is responsible for loading, managing and providing message passing services between any pair of child components. The host can in principle be a component of a parent host. Together they form a loosely coupled, dynamic object graph. The design of the messaging services should not result in implicit tight coupling between the supposed to be loose coupling components, e.g. a straight forward use of imperative delegate/event has no chance of preventing implicit tight coupling and the formation of potential deadlock or infinite loop due to bad design or the lack of implementation details of other components. Asynchronized message passing mechanism, in which the host acts as a mediator, is desired.
- The protected assemblies are loaded from general streams, which eventually end up as byte arrays. Due to security considerations, the assembly loaded from a byte array is treated differently from the ones loaded implicitly or explicitly from the local disk file in .NET Framework and should also be treated differently in the present framework. It is found that a plug-in framework fits the needs.
- The plug-in framework has to be flexible in sense that:
- the protected assemblies holding the child components are loosely coupled to the host via a programmable interface conforming sockets (not the network socket:-)) that can be loaded and unloaded on-demand at runtime. They can also be pipelined if needed.
- the host should not be tightly bound to a set of child component with specific versions as long as certain interface (contract) is not changed.
- The host can act as a scripting engine for .NET languages. The unload capability provides a necessary condition. This is important for server applications and real-time debugging scenarios in which the host supports many child components during its life time that cannot or are too expansive to be stopped, but some of these children need to be changed (at the source code level!) in the meantime.
Flexibility is required also because of the following. First of all, they are generally considered the best approach to the development of distributed service/smart client applications by many developers that are loosely coupled themselves. The technical advances at CryptoGateway makes the real life deployment of such systems a step closer to reality.
- Allows pipeline factorization in which a socket can be associated to one to any handlers arranged in a particular sequence that implement a particular interface (see Fig. 1).
- etc.
There are many plug-in frameworks belonging to a variety of design patterns for .NET, this article is not going to discuss or compare any of them due partly to the length limit of the article. The way in which the host realizes the asynchronized message passing between its child components is also not to be covered here. A schematic UML diagram for how the child components are connected to host and each other that can satisfy the above requirements is shown in Fig. 1. There is a one to many relationship between the interface and the sockets and the components. A component is part of a socket. Each socket can allow a sequence of one to any components forming an ordered processing pipeline, to be plugged in. It is explained in more detail in the following.
B. The Connection
This framework is interface based. The top level development "flow chart" is shown in the following:
Step 1: Interface Specification
The first step in the attempt to connect a component to a host that is previously unknown to the host is to provide an interface that declares the properties and methods that are open to the host and a set of sockets (see the following).
Interfaces are introduced at least in two ways:
- In top down development oriented processes, they are designed first and then implemented. Then the process is iterated when a need is generated in the implementation process. This process requires the designer to have sufficient pre-existing knowledge pertaining to the component gained from previous experience or from prototype building.
- In bottom up development oriented processes, the component is developed first or is pre-existing. They needs to be plugged into the host. In this case, the exported properties and methods should be identified first and the corresponding interface produced as a generalization. The process is also expected to be iterated until the overall designed goal is reached.
Step 2: The Component Socket
After the step one is done in a particular iteration or if the component implements a existing interface and either it is the first attempt to be plugged into the host or it is to be plugged in to a new kind of socket, then the socket has to be designed and implemented.
A socket acts as a dispatcher for call requests from the host to the component (sequence) derived from the same interface. It is also responsible for loading and unloading assemblies that contain the implement. It is initialized from an XML configuration "script" either in an execution branch reached prior to or just before one of the components handled by it is needed. The socket should be created in a separated AppDomain
in order for the host to be able to unload the assembly set belonging to it. It should also be derived from MarchalByRefObject
(or the equivalent of it) so that the host can communicate to it via object (remoting) proxies. The information initialized at this stage belongs to the socket class that is common to all instances of it. An instance of it is created in its own AppDomain
to facilitate the initialization (of static
member variables) and is then discarded without creating any component. 6-7 inside the sequence diagram on the right (Fig. 2) represents the initialization process.
When the need to use one of the components arises, an instance of the socket that holds it is created first and then the corresponding set of assemblies are loaded by it. The load process, opaque to the host, is represented by call stack 9->10->11->12 in Fig. 2, which is discussed in the following. The Secured Assembly loader is itself a socket holding the client of a process server inside crypto-gateway server where the authentication and decryption is performed. After the corresponding assembly is loaded, the set of instances of the types that is configured to be attached to the socket is created, which is represented by the call stack 13->14 in Fig. 2. After the socket is created, all the objects that are configured to attach to it are also created. The type of these objects and the objects themselves are used to access their members via reflection. More details are provided in the following.
The method call and property access can now be delegated to the component object (objects pipeline, if so configured) by the socket, which can also be configured to perform logging, exception trap and handling, etc. The interface can also define a set of events for the components to call back to the host, which could:
- handling it
- delegate the call to other child components
- propagate it upward in the host hierarchy
The synchronized version of it is illustrated in Fig. 2 by 20->...->27. For asynchronized version, which is suggested above, call 20 returns (26->27) immediately after 21, leaving the host to handle the message. A typical socket call handler looks like:
namespace Media
{
public class Mp3PlayerSocket : IStreamClient
{
.....................
public void LoadPlayList(System.Collections.ArrayList list)
{
try
{
if (!pipeline)
{
if (miLoadPlayList==null)
miLoadPlayList = otype.GetMethod("LoadPlayList",
new Type [] {typeof(System.Collections.ArrayList)});
miLoadPlayList.Invoke(o,new object [] {list});
}
else
{
foreach (string key in ttable)
{
otype = atable[key] as Type;
ArrayList item_list = new ArrayList();
miLoadPlayList = otype.GetMethod("LoadPlayList",
new Type [] {typeof(System.Collections.ArrayList)});
o = ol[key];
miLoadPlayList.Invoke(o,new object [] {item_list});
list.AddRange(item_list);
}
}
}
catch (Exception ex)
{
LogError(ex);
}
finally
{
................
}
}
private static MethodInfo miLoadPlayList = null;
..........
}
}
where IStreamClient
is the interface. A typical socket configuration file looks like:
="1.0"="utf-8"
<config>
<sactserver ip="127.0.0.1" port="1221" path="/"
channelexpires="120" channelpersists="false" />
<holder>
<assembly secured="true"
default="true" feature-index="0"
atoken="/MediaPlayer/MediaClients_0.ctk"
id="e6f511d2-f9f9-4ad6-b700-2505b92c64dc"
depend_id=""
name="MediaClients, Version=0.2.2059.28511,
Culture=neutral, PublicKeyToken=null"
display="Media Clients">
<description />
<server ip="127.0.0.1" port="1221" />
<gui type="Media.Mp3Player,Mp3Player" show-background="images\Img12345.jpg" />
<interface type="Media.IStreamClient" wrapper="Media.Mp3ClientSocket" >
<type name="Media.Mp3Client" activate="true" ext=".mp3" descr="MP3 Audio"/>
<type name="Media.Equlizer" activate="false" ext=".mp3" descr="MP3 Filter"/>
</interface>
</assembly>
</holder>
</config>
Step 3: The Implementation
The implementation is then performed. During the process, a need may arise to go to a previous step.
Step 4: The Test Process
Then the test is carried out. During the process, a need may arises to go to a previous step.
Step 5: Done?
If not, go to a previous step, otherwise exit the process.
C. The Realization
Most of design goals discussed above are realized and tested in various .NET programs of CryptoGateway that are in private use, under evaluation, or for public download. Although not a single one contains a realization of all of them, their combination does not have foreseeable obstacles.
Assembly Protection and Distribution
An Introduction
The data access control technology of CryptoGateway is based on a declarative identity management and access right distribution system using cryptographic (RSA and AES) means. It can be applied to any digital data. Access right is distributed using soft tokens containing authentication information about the producer and the user through conventional communication channels like e-mail. The soft access token is secured using the same access control method. The production and distribution process is schematically shown in Fig. 3. For more details, interested readers should read articles following the links given above.
.NET Assemblies
They are not special in terms of crypo-images production, access token distribution and user access. More details on how this is accomplished is given here. What is special is the client, called "Secured Assembly Loader" in Fig. 1, of the process server hosted by the crypto-gateway server. It is specifically designed to communicate to the said server to load .NET assemblies, to connect to the authentication user interface, etc. The one for the media player is written in C#. The process server does the actual job of authenticating the user. It also makes sure that the data is indeed signed by the producer in the background. Valid data will be loaded only after both the user is authenticated and the producer's digital signature is verified.
After the data is loaded by the above mentioned loader, it will try to generate an assembly by calling:
Assemb assembly = Assembly.Load(data);
Type type = assembly.GetType("...type name ...");
ConstructorInfo cinfo = type.GetConstructor(....);
object obj = cinfo.Invoke(args);
....
MethodInfo minfo = type.GetMethod(...);
minfo.Invoke(...);
....
PropertyInfo pinfo = type.GetProperty(...);
pinffo.GetValue(obj,...);
pinfo.SetValue(obj,...);
......
FieldInfo finfo = type.GetField(...);
finfo.GetValue(obj);
finfo.SetValue(obj,...);
......
where "data
" is a byte array containing the serialized assembly data recovered by the process server. After the assembly is successfully constructed, reflection can be used to create instances and access their members. Unloading of this assembly is also possible by calling the Assembly.Unload()
from the host after all threads in the components are stopped.
In the design stage, various functional units used by the system should be partitioned into to a set of assemblies taking into account the fact that .NET assemblies are not a MarshalByRefObject
so each AppDomain
must have its own version of an overlapping assembly. To reduce redundancy and potential inconsistency, an effort to reduce or eliminate the overlap is better to be made.
The factorization should also be geared towards logical separation of responsibilities. The host, as an upper layer, should treat various components in as few different ways as possible, which can be achieved by making the components as self-sustaining and self-describing as possible. This is called the principle of "maxium symmetry" in this article. A reader might encounter similar principle in studies on information/statistics related subjects under the name "maximum entropy" with its roots in statistic physics (try to search this site, a reader can find a few). "Symmetry", which is related to certain kind of invariance, is used because something is known as going to be put in a proper order. It is a better term for engineering. "Entropy", on the other hand, is related to things that are unknown that are going to be known or learnt. In fact, better learning strategy also involves putting what's needed to be known next in a proper order, better engineering should also admit that there are unknowns. On an artistic side, the principle could result in greater simplicity in code. On a practical side, it leads to increased code reusability (invariance!), reduced maintenance cost and opened possibility for future expansion. The practice following this principle is also expected to result in a increase of code performance because passing messages (call request) through socket->host->sockets layers and AppDomain
boundaries is not expected to be efficient.
Admittedly, the above formalization process generates a significant number of higher-level assemblies and classes (interfaces, sockets, configuration files, etc.) from classes where the real job is done. The synchronization between these classes in the iterative code development process is tedious and could be error prone. This pre-processing job can in fact be automated by taking advantage of the reflection capability of .NET and other frameworks. The use of software tools, like the x-script generator, can greatly simplify the tasks and reduce the possibility of code inconsistency. The post-processing, including compiling, signing of digital signature, encryption, initial access token distribution, registration, packing, uploading, etc., can also be streamlined using these tools too.
Points of Interest
It can be expected that the application of the same idea to Java assembly (bytecode) is straightforward since these two frameworks share a lot of similarities in the aspects that are relevant to code protection and the plug-in (-out) framework introduced here.
As a reader already knows, this article is written in as general a form possible, it provides an ideal or background reference that hopefully will result in designs and structures that allow later transition to a more flexible system with less effort for those who use it. For simple applications, it is an overkill. In actual implementations, depending on the scale, flexibility, and future expansion requirements, a lot of the socket layers, interfaces definitions and code protection steps can be skipped or short circuited, at least in the early rounds/versions of the system, only taking into account the possibility of expanding it is required during the factorization process.
To Do
Continue expansion of the "etc.", etc.
History
- 25th May, 2006: Initial post