Contents Home Page
This chapter discusses the concepts that a programmer needs to understand when developing an application for the E runtime environment. In many ways, the E runtime environment is similar to the Java runtime environment; however, this chapter points out several distinctions.
Topics covered in this chapter:
Comparison to Java
Optimistic programming techniques
Handling timeouts
Persistent object storage
Closures
Exception handling
Creating access-controlled objects
Authentication
E offers several enhancements to Java. While summarized in the Getting Started with E chapter, they are explained in detail in this section.
- Optimistic computation. Applications executing in a network environment can be slow and inefficient because of inherent network latency. E's model of messaging and optimistic computation helps optimize your distributed applications and make them operate more effectively over the network.
- Improved security through capability semantics. E uses the concept of trust to restrict behavior while maintaining effectiveness at runtime. E grants specifically-requested capabilities to an object at load time, and those capabilities are not checked again. Imported objects can carry a cryptographic signature, which ensures that they have not been tampered with.
- Distributed communications extensions. The network becomes almost transparent to an E programmer. The E runtime takes care of details related to network protocols and intersystem transmissions. Programs can pass references to E-objects across the network, and the form of those references is the same whether the programs are on the same or remote machines.
- Distributed garbage collection. E implements an effective, transparent distributed garbage collection system that is not confused by object references crossing machine boundaries.
Optimistic computation
This section describes E's implementation of optimistic computation, which improves efficiency in a distributed environment. Optimistic computation in E lets your program send a message to an object requesting a value, and still continue executing the rest of the code "optimistically" as if it already has the answer it needs; it does not need to wait for a response to continue processing. When the requested value is supplied, the associated code block can execute.
Optimistic computation helps avoid long delays when communicating across a network. E implements optimistic computation through its messaging facility, channels, and deferring statements.
Messaging in E
Messaging is not unique to E. Both E and Java use the concept of messaging for communication among objects. When object A wants object B to perform one of its methods, object A sends a message to object B. This message contains the object to which the message is addressed, the name of the method to perform, and any parameters needed by this method.
However, messaging in E does have distinct differences from Java:
- You send a message to an E-object using the send operator (<-), rather than using Java call/return semantics with the object.method syntax. Messages in E do not have return results; they are merely sent. In Java, you call a method and wait until a return value arrives. In E, you send your message and forget about it, assuming that the message will arrive and be acted upon.
- An E-method can only be a private void method, which means you cannot access them through normal Java programming techniques.
- E messages also contain a sealer, which is an object representing the right to send a message to another E-object. A sealer secures an envelope encapsulating the parameters of a message, and ensures that this message can only be read by an object with the appropriate unsealer.
- When you send a message referencing a Java object over the network, the entire object gets copied over. E, on the other hand, lets you send just a reference to the remote E-object.
- Messaging in E is one-way and asynchronous. This means that recipient E-methods, by default, might not furnish requested values in the same order that the corresponding E messages are sent (although you can force them to do so). Also, requested values from multiple objects can be queued up through their channels (see the following section on Channels) and delivered to the requesting object all at once.
To make a simplified analogy, E's messages are similar to ordinary letters you drop in the mailbox. Like the Post Office, the E runtime informs you only if the message is undeliverable. If the delivery goes normally, you don't know when a message arrives, and you don't know when the recipient will respond to it, assuming it requires a response. You simply drop the letter in the mailbox and continue doing other things, in the expectation that the mail will be delivered and the recipient will respond.
In contrast, conventional Java method call/return procedures are like dialing someone on the phone. You know when you are successfully connected with the person you've phoned, and they respond in real time. The downside is that neither of you can do anything else while you're engaged in that phone call. A method making a Java call can't perform any other computations until that call is completed.
NOTE: Either a Java or E-object can send a message to an E-object. However, the recipient must be an E-object.
Example of E messaging
This example shows the familiar Hello World program, written in both Java and E. While this is a very simple illustration, it introduces you to the difference between messaging in E and Java.
In Java. This example shows Hello World written in Java.
public class Hellojava { public static void main(String args[]) { HelloWorld first = new HelloWorld(); first.hello(); } } class HelloWorld { public void hello() { System.out.println("Hello World"); } }This program creates a new HelloWorld called first. It calls first's method hello, and waits for hello to print "Hello World".
In E. Here is the same program written in E.
public class HelloE { public static void main(String args[]) { HelloWorld first = new HelloWorld(); first <- hello(); } } eclass HelloWorld { emethod hello() { System.out.println("Hello World"); } }This example program creates a new HelloWord E-object called first. first is sent the hello message, and responds by executing its E-method called hello. In contrast to Java, however, the program does not know when, if ever, first will respond.
Channels
In E, you can never know when, if ever, a recipient E-object will furnish a value. In fact, you might send a message to an E-object that is not even yet instantiated. To let your program continue executing yet retain a placeholder for a requested result (if it ever arrives), you include a channel as one of the message parameters.
A channel is a conduit object (of EChannel) carrying messages from one object to another, and acts as a placeholder for any values that might be furnished from the recipient E-object. Every channel has a distributor, which is an object (of EDistributor) that tells the channel where to send its messages. A distributor is like a nozzle on the end of a hose that directs the message to its destination.
To obtain a channel's distributor (in order to tell the channel where to send messages, for example), you prefix the ampersand symbol (&) to the channel name. For example &mychannel gets the distributor for the channel mychannel.
To tell a channel where to send its encapsulated messages, you send a forward message to the channel's distributor, enclosing the destination E-object(s) as one of the parameters. The requesting object can then use the channel as a reference to the requested result.
This example sends a forward message to a distributor MyResult. The forward message tells MyResult to send the messages in its associated channel to an E-object called Answer:
emethod DieRollEwith (Edistributor MyResult) { MyResult <- forward (Answer); }A channel can be forwarded to multiple E-objects, which is called a multicast channel. Any message sent through that channel will be received by all the destinations.
E's deferring statements
As stated earlier, you can never know when, if ever, an E-object will furnish a requested value. In order for your program to continue executing and not hang, E's optimistic computation lets you defer execution of a certain block of code until a value is furnished for an E-object. This lets your code continue executing other parts of the program "optimistically", as if it already has the values it requested.
E implements this deferring feature through several E statements: eif, ewhen, ewhenever, and the ecatch portion of etry. These statements let you both establish the conditions under which blocks of code will be executed, and also link E-code to objects that should not or cannot execute optimistically.
When your E program encounters one of these statements, it takes a snapshot of all the referenced local variables and parameters within the scope of that statement. This snapshot is called a closure. E saves this closure for whenever the deferring statement needs it. When the requested result is furnished, the deferred block of code executes, using the values in the closure.
All other non-deferring statements in the code block which contains the deferring statement (including statements which follow the statement in the source text) execute before the code blocks associated with the deferring statement, regardless of when the value is produced.
In the following example, an object sends a message to another object requesting a value, but can continue computing until the result is furnished. At that point, the object prints the result on the console.
someObject <- doSomething(&aResult); ewhen aResult (int theResult) { System.out.println("The result is " + theResult); } keepGoing();Note that even if a value for theResult is produced immediately, allowing the ewhen block to execute, the program executes the keepGoing() code first.
For more information, see the section on Closures later in this chapter, and also the documentation on the deferring statements in the E Language Specification chapter.
How optimistic computation helps your distributed applications
E's implementation of optimistic computation is ideal for applications that contain many objects needing to communicate simultaneously over the net. Optimistic computation cannot eliminate network delays for a single transaction, but it does allow the program to queue up messages (and responses) without invoking a new latency "penalty" for each and every one.
A requesting object can send messages to a channel immediately, even before the actual requested value has been determined. This lets the sending object continue executing without having to wait for a response from the recipient E-object. E-methods operate over the net asynchronously; they do not need to execute in a predetermined order, although they can be forced to do so.
Since computation continues, E programs can send more messages over the network in a given period of time than conventional Java methods, which use call/return semantics. E also includes an exception handling mechanism to deal with bad messages or return values that never materialize.
Another advantage of E's messaging is that it eliminates many unnecessary roundtrips across the network. You can nest channels within channels so that the anticipated result is altered "through the pipeline," before it is actually received. For example, your program could send a series of messages with commands such as "multiply these," "add one to the result," "send your result to the following object" through a series of channels, without ever requiring a packet to be received back across the network.
E's message passing semantics also help avoid deadlock and race conditions by enforcing these two rules in the E runtime:
- No guarantees are given about the order of message arrival.
- Only one method at a time may be executed by a single instance of an E object, if that object contains mutable state information.
By operating under these assumptions, the E environment guarantees that object state information will remain consistent and that processes can execute to completion.
Improved security through capability semantics
E's security model is based on the use of capabilities, which grant permissions to send particular messages to particular objects in a distributed system.
Controlling which capabilities an object is given lets you restrict access to sensitive system resources (such as files or data communications lines). For example, you could give an object the capability to add a limited number of records to a database, but prevent it from writing anywhere else in the file system.
Capabilities are assigned directly to an object. This means that they are preserved regardless of where the object originates-whether on the network or on your local drive. You do not have to rely on third-party security mechanisms to assign privileges for your application.
By creating a trusted object that implements limited capabilities, and giving an untrusted object the ability to send messages to this new object, you can obtain more functionality from the untrusted code without compromising security.
This security model is different from Java, which has bases its security on an "all-or-nothing" approach: it assumes all code originating on the local hard disk is trusted, and all code that originated from the network is not trusted. This model can be very secure, but it does have loopholes. For example:
- Ideally, there should be nothing a programmer can code that grants access privileges to an object that have not been otherwise granted. In Java, however, simply knowing the name of a public class is enough to access to that class's functionality-all you have to do is type the class name. In E this is not the case; you can grant an object specific access privileges to other objects.
- Similarly, specifying a file name when using the Java File class lets you refer to any file. This file name is then passed to the operating system, which does not know what you are going to do with the file, and must determine your access based on arbitrary, unrelated criteria, such as your user name.
E, in contrast, encapsulates all references to resources in objects which cannot be forged or synthesized by user code. You can restrict an application to access only certain files, and perform only certain operations on these files.
Every piece of code executing in the E system must be "vouched for" by a cryptographic signature that can certify its integrity. This ensures that an object's security cannot be compromised by unauthorized modifications to its class definition or package. According to the "trustedness" of the signature, a set of access capabilities can be given to a class as it is loaded; these capabilities need never be checked again. To create the trust relationships among objects, E provides the Class Blesser. See the section on Authentication and the Class Blesser for more information.
Another kind of capability within the E system is called a sealer, which is a permission to send a particular message. The objects that receive the message must have the corresponding unsealer. The sealer/unsealer objects are built into the E-object architecture. Each sealer/unsealer pair operates like a public/private key pair in public key cryptography; that is, if a given unsealer can successfully decrypt a sealed message, it is a guarantee that the sender was "legally" entitled to send that message.
Distributed communication extensions
E features a built-in model for remote object communication, that lets you automatically establish connections between objects without having to deal directly with the underlying network protocols. Any Java or E-object can send a message to another E-object, even if that E-object is on a different machine on the network. This lets machines talk directly, with messages directed only to specific objects.
Other languages can require a lot of effort to establish remote communications. For example, Java requires many sockets to be built just to establish a pipe between two machines, and the result is not as secure. In contrast, E lets you do this easily with just a few keywords.
Because many of the communications details are already built into E, you don't have to manually code things like:
- Streams and threads.
- Network access details like socket interfaces, pipes, or any other underlying network protocol.
Distributed garbage collection
The E runtime provides a distributed garbage collector. Unlike Java's garbage collector, which performs garbage collection only within the scope of your local machine, E's distributed garbage collector performs garbage collection over the net for your distributed objects. For information on using the distributed garbage collector, see Appendix A.
Using Java and E Code together
Java and E code can coexist in the same program-you can write Java classes in an E program, and implement private Java methods within E classes. In fact, there are situations where you will want to use Java code in your E program in order to gain maximum efficiency. This section provides some guidelines when to use Java code instead of E, and vice versa.
When to use Java
Using Java code can be more efficient when you are developing code to address the following situations:
- Efficiency on local machines. When you need the best performance from your program, and the code only runs on a local machine, you may want to use Java classes, since they function synchronously. Synchronous execution dedicates all the system resources to the executing code until it completes its task.
- Streamlining code. When you do not need E semantics (for example, with simple structural classes), there is no need to use E code. Java code is sufficient in some situations, and can use less resources.
- Synchronous processes. Use Java if your code needs to deal with resources or objects that cannot operate asynchronously. This is because E methods can only operate within an asynchronous environment. An example is when you need to read from a blocking stream that might cause your thread to suspend until input is available. You should never suspend inside of an E-method or perform any operation which may result in your thread blocking from within an E-method.
- Non E-supporting frameworks. Use Java when your code must execute within another framework which precludes using E. For example, to display a modal dialog using the Java awt package, you must use the special awt thread fielding UI events, such as button presses. Since there is no guarantee inside which thread an E-method executes, you cannot show a modal dialog inside of an E-method.
You can perform the following operations from within methods defined in Java classes:
- Create E-objects.
- Obtain E-distributors from E-objects created within the current method scope.
- Send E messages to E-object.s
- Throw E exceptions.
- Forward E-objects to E-distributors.
NOTE: You can only implement private Java methods within E-classes; no other access types of methods are allowed. Also, be aware that you cannot call these methods directly from within E-methods if they either are synchronous or must run in specific threads/environments.
When to use E
Use E, on the other hand, to address situations such as the following:
- Asynchronicity. Use E classes to implement asynchronous behavior. E classes provide asynchronicity with optimistic computation, asynchronous messaging, and asynchronous exception handling.
- Forwarding. To forward an object to an E-distributor, the forwarded object must be an E-object.
- Distribution. To distribute references to an object over the network for messaging purposes, you must use references to an E-object.
- E operations not supported in Java. If you are using the following E operations, code them in E classes; these operations cannot be run from within Java classes (although you can call them within Java methods in E-classes):
- Distributed references. You can only distribute references over the network to E-objects. Since all Java objects are sent over the network by copy, distributed messages over the network can only be sent to E-objects.
To write code for a program that will compute optimistically, break the problem down into three parts:
- First, write the code exactly as if you already have all the values you need for the computation.
- Next, create message protocols to deliver and receive the values you need. Set up channels (usually unforwarded at this stage) and distributors to move the information when it becomes available. This code goes at the beginning.
- Write ewhen, ewhenever and eif statements for each value that you assumed you had in Step 1. These statements go after the protocols and encompass the body of code you wrote in Step 1. They enforce security and ensure that the code which uses a value you assumed doesn't execute until after it actually gets furnished that value. Protect yourself against the possibility you may not receive the information you need in a timely fashion by creating guarded-if constructs to encapsulate your information requests. (See the description of eif for an example.)
Example
For example, to write a program in which players A and B each roll a die, exchange the values of their rolls, and receive the outcome of their combined rolls, you might do the following:
- First, write code that decrypts the value of one player's roll using a key sent by that player, combines the decrypted value with the value of the other player's roll, and calculates the outcome.
- Next, write the protocols to perform the following steps:
Select a random number X.
long X = randomGenerator.nextLong();Reveal the one way hash of X.
myHash<-forward (new EInteger(OneWayHash(X)));After receiving the other's hash, reveal X.
myX<-forward(newEInteger(X));After receiving the other's X, combine the Xs to produce the final result.
long finalResult = Combine(X, otherX);Verify that there was no cheating.
if (OneWayHash(otherX) != otherHash) { System.out.println("There was cheating.");- Write deferring statements (such as ewhen and eif) to ensure security and guarantee that computations do not take place until expected values are provided. For example, to prevent a player from sending its key until the opponent's hashed value has been received, you could use this code:
ewhen hisHash (long otherHash) { myX<-forward(newEInteger(X)); ....Similarly, the following statement prevents a player from generating the final combined result until the opponent's X number has been received:
ewhen hisX (long otherX) { long finalResult = Combine(X, otherX);Common pitfalls
- Avoid the recursion pitfall of having message forwards spawn uncontrollably.
- Watch out for side effects from ewhen, ewhenever, eorwhen, eif and eorif clauses due to modification of instance variables. See the section on Closures for more information.
To maintain a robust and reliable distributed environment, you must incorporate good timeout procedures in E-methods. That way, E-methods that have sent messages requesting values will not wait forever for those values to be provided.
You should always take the time to include reasonable safeguards in your application. A user may engage the services of hundreds, if not thousands, of objects over the course of a work day. No programmer is infallible; the odds are that the user will eventually encounter an object which does not function perfectly all the time. If you do not plan for that possibility, you will not have a reliable system.
In E, there are two general types of timeouts to be aware of:
- Mailing timeouts
- Response timeouts
Mailing time-outs
These occur when the system cannot successfully deliver a message to the recipient object within a predefined time period. The communication library E-classes handle mailing time-outs by throwing a message exception. You do not need to generate any code yourself to deal with this type of timeout.
Response time-outs
These occur when a message is successfully delivered to an object, but the receiving E-method does not respond in a timely fashion. E's runtime does not throw an exception here, since this is a legitimate situation. This is because E's messaging model is asynchronous; there is no way for a sending E-method to know when its requests will be serviced. You must develop your own procedures to handle these types of timeouts.
Handling this type of timeout is especially important for a program that is not "time-dependent". For example, an automated stock market broker or mouse event responder never can know when, if ever, it will receive a response. Since these programs may never "complete", you should specify a termination procedure in the application-a mutually-agreed-upon handshake or procedure for acknowledging the receipt of messages-to let it shut down or keep running when it encounters a timeout. Otherwise, you will not know what, if anything, is wrong at the recipient's end that could be preventing it from responding appropriately.
There are two ways of creating a timeout-handling procedure:
- Include fall-back procedures for the requesting E-method to follow when a value is not received within an acceptable time period.
- Include procedures for the responding E-method to ensure that some response is sent to the requestor, even if the recipient cannot provide the requested values in a timely fashion. This implementation is generally more feasible since, as the stockmarket/mouse event example suggests, the requesting E-method cannot always tell when a message has been received.
To create a timeout handling mechanism, you can use the functionality in the RtTimer class. See the documentation in the E Class Library chapter on this class for specific information on its technical implementation.
The following programs are examples of using RtTimer to create a timeout procedure.
Example 1
eclass ThingThatHandlesTimeouts { emethod doSomething (EObject other) { EBoolean timeout; EInteger result; RtTimer timer = new RtTimer(); int tid; // set timeout tid = timer.setTimeout(1000, &timeout); // Ask some other object to do something for us other <- askForResult(&result); // Other object notifies us it is done // by forwarding something to result ewhen result (int res) { timer.cancelTimeout(tid); // We don't need it timer.terminate(); System.out.println("Got result: " + res); } // We'll enter here if we timeout before // the other responds eorwhen timeout (boolean t) { System.out.println("Timed out"); timer.terminate(); } } }Example 2
eclass SmartThingThatHandlesTimeouts { emethod doSomething (EObject other) { EBoolean timeout; EBoolean cancel; EInteger result; RtTimer timer = new RtTimer(); boolean waiting = true; int tid; // set timeout tid = timer.setTimeout(1000, &timeout); // Ask some other object to do something for us other <- askForResult(&result); // Other object notifies us it is done // by forwarding something to result ewhen result (int res) { waiting = false; // In case timer fires now timer.cancelTimeout(tid); // We don't need it timer.terminate(); System.out.println("Got result: " + res); } // We decided to cancel (see timeout code), // stop waiting for the result (note this is eORwhen) eorwhen cancel (boolean c) { System.out.println("Cancelled"); timer.terminate(); } // Here is the timeout handler ewhenever timeout (boolean t) { // Make sure result didn't just come in before we // could be cancelled (check waiting flag). The call // to cancelWaitingForResult() tells us if // we should cancel or not - // that could be an alert panel to the user, // or use some heuristic to realize we're tired of // waiting for result... if (waiting && (cancelWaitingForResult() == true)) { // This causes the eorwhen to fire above &cancel <- forward(etrue); } else { // Reset the timer timer.setTimeout(1000, &timeout); } } } }
E provides a persistent object database object called PObjDB to store objects to and reclaim them from a database. This lets you store your objects to disk and later retrieve them in other applications. You must create your own storage and retrieval procedures using with PObjDB; E does not do this automatically.
An example of creating an object database is at the end of this section.
How E stores objects in a object database
To store and retrieve objects, E uses a streaming protocol that is a subclass of the streaming protocols already implemented in Java. When a program requests object storage, and the object already implements a stream-out method, E uses that method. If not, E uses a generalized native method instead. To restore, E uses a stream-in method (local or native).
E currently does not store sufficient meta data to the stream to handle class implementation changes. This means that an object database you create today will not work tomorrow if you change the instance variables of the classes involved.
Stream keys
To store an object in a persistent object database (called a database in this section), create a procedure in your E program to explicitly request this storage. When your program makes such a request, it receives a token called a stream key. Your E program can then later redeem this stream key for the object.
This stream key can be stored in other objects, and these objects can also be stored out under other stream keys.
You can specify a name for a stream key when you save it to the database. This name is called a root key. Later, when the same or another E program opens the same database, the program can retrieve the steam key using this name. That program can then redeem the stream key for other objects, which in turn may contain more stream keys. You can continue this process as needed to reconstruct an elaborate network of objects.
You can save any E or Java object to the object database. Also, any E or Java object name can be the root key name.
Parent databases
You can create an object database that indirectly refers to another object database. The database that is referred to is called the parent database. Your actual production database is called the child database. If you do not define a stream or root key value for the child database, the parent handles the request instead. Similarly, changes made to a child will mask values held by the parent.
Using child/parent databases is useful for modeling nested transactions. For example, you can use the child database as a testing environment for making changes to objects. When your modifications are complete, you can commit your changes by applying them to the parent.
Filters
To grant privileges for reading, writing, and committing changes to a database, you can use objects that implement the RtDBViewFilter interface. This lets you restrict other users' access to your object database. Currently, the E runtime system has two classes that implement this interface:
- PObjDB, which grants full access.
- RtDBViewLimiter, which lets you selectively assign read, write, and commit permissions, along with basic key list control of root key manipulations.
How the RtDBViewLimiter policies work
The grantor creates a RtDBViewLimiter object, initialized with the policy control parameters desired. This can be passed on to the grantee, who will be restricted to the operations allowed. These objects can be chained, with each link further limiting the powers of the ultimate grantee. Trying to establish a less restrictive policy will fail because all operations pass control back through the delegation chain.
There are three master control flags:
- readPolicy
- writePolicy
- commitPolicy
There are also two key control lists, which control which root keys the grantee can read or change. A null key control list means that all access is permitted. Otherwise, the list restricts the user to read or store only the specified root keys. These lists do not affect access to stream keys, which are considered to be capabilities in themselves.
Exceptions
Violating key control policies results in the DBAccessException being thrown. When allowed, a request for a non-existing value is handled by returning null, rather than throwing this exception.
Class-specific stream-in and stream-out methods
You may want to define your own methods for storing and filing a class state to and from the stream objects used to implement persistence and communication. This can be more efficient than relying on the native method stream-in and stream-out code. It can also be more robust with respect to instance variable type and position changes. For example, you can include code for your class that recognizes older versions streamed out long ago.
To do this, declare your class as implementing the RtCodeable interface. This interface extends the RtEncodeable and RtDecodeable interfaces, which contain the encode and decode methods, respectively. These methods call appropriate read and write methods on the RtEncoder and RtDecoder objects to save or load internal state. These classes implement the standard Java DataInputStream and DataOutputStream protocols, along with some extensions for reading and writing objects.
Note that the decode method is expected to return the object decoded, which should usually be yourself. (In some special cases this object can be different from the object sent the decode message, if necessary).
If you only want the object to be encodeable, have your class only implement RtEncodeable, which contains the encode method. Likewise, to have a class only be decodeable, implement the RtDecodeable interface, which only contains decode.
For example, assume you encode an object of class A so that it is decoded as an instance of a different class B. However, you might not want to force instances of class B to be encodeable just because you want to decode them; in other words, you want objects of class B to be sent over the network by reference only, even though they can show up as a copy when an A is encoded.
To do this, have class A implement RtEncodeable, and class B implement RtDecodeable. This lets instances of A be encodeable, but only lets instances of B (encoded from an A) be decoded, and not encodeable themselves.
Example
package ec.examples.dbe; import java.lang.*; import ec.e.db.*; import ec.e.stream.StreamDB; public class DBexample { public static void main (String args[]) { String dbFileName; if(args.length > 1) { StreamDB.setDebugOn(); } try { System.out.println("Creating PObjDB"); if(args.length == 0) dbFileName = "/tmp/ExampleDB"; else dbFileName = args[0]; PObjDB btdb = new PObjDB(dbFileName); System.out.println("DB created named: " + dbFileName); Object cat1[] = new Object[2]; Object cat2; cat1[0] = "Meow"; cat1[1] = new Integer(123456); System.out.println("Storing first test object."); RtStreamKey catKey = btdb.put(cat1); System.out.println(catKey); System.out.println("Closing root PObjDB file"); btdb.closeDB(); System.out.println("Reopening root file with an update view."); PObjDB db1 = new PObjDB(dbFileName); PObjDB db2 = new PObjDB(db1); cat2 = db2.get(catKey); System.out.println("Retrieved object " + cat2); System.out.println("Writing a string named DogName to the update view"); String dog1 = new String("Woof"); RtStreamKey dogKey = db2.put(dog1); db2.put("DogName", dogKey); System.out.println("Committing the update view back against the root file."); db2.commit(); db2.closeDB(); db1.closeDB(); System.out.println("Reopening the root file using a read only view limiter"); PObjDB db3 = new PObjDB(dbFileName); /* Now open the view limiter */ RtDBViewLimiter readOnlyView = new RtDBViewLimiter (db3, true, false, false, null, null); RtStreamKey dog2Key = db3.get("DogName"); Object dog2 = db3.get(dog2Key); System.out.println("Object retrieved under DogName is " + dog2); System.out.println("Attempting to write to the readOnlyView"); System.out.println("(This should produce a DBAccessException)"); readOnlyView.put(new String("BadGuy")); System.out.println("Security failure"); } catch (Exception e) {e.printStackTrace();} } }
In E, you can use certain statements to defer execution of a block of code until a certain value is furnished. These "deferring" statements include eif, ewhen, ewhenever, eorwhen, eorif, and the ecatch portions of etry.
When your E program encounters one of these statements, it takes a snapshot of all the referenced local variables and parameters within the scope of that statement. This snapshot is called a closure. E saves this closure for whenever the deferring statement needs it; the statement then uses the values in the closure when it executes.
Each deferring statement gets its own closure. For an ewhenever, each execution operates on a copy of this closure. This ensures that the closure retains its original values for the next ewhenever execution. If your ewhenever statement modifies any of the values in the closure, those new values are not retained.
Be aware, however, that if your deferring statement references Java objects, the closure captures the reference to that object, not the value of the object itself. This is especially an issue for ewhenever statements, which can have multiple executions. If your program subsequently modifies the object, the closure will reference that modified object, and the next ewhenever will execute using the new value.
For a description of E's exception handling environment, see the documentation for the etry statement in theE Language Specification chapter.
E applications are run inside of an environment provided by the E runtime. Within this general environment, you can use a special set of E classes and methods to create customized restricted environments, and pass these to less-trusted objects. This lets you take full advantage of the security features of the E environment.
Currently, E provides classes and methods to set up the following facilities:
- File system. The E Environment provides file system capabilities wrapped within an RtFileEnvironment object.
- Network communication. The E Environment provides network communication for distributed objects wrapped within an RtNetworkEnvironment object.
NOTE: E also provides a set of wrapper classes that implement timing mechanisms. However, these are not described in this section, since you do not use E's launch facilities to implement them. See the documentation on Timeouts (in this chapter) and also the descriptions of RtClock and RtTimer in the E Class Library chapter for more information on these classes.
You use these classes as "factories" to create other objects representing more specific capabilities, such as a file or network directory server. Because the E environment capabilities are wrapped in these standard "factory" objects, you can keep class constructors private. This means that other objects accessing the class you create cannot arbitrarily create instances of that class to bypass any restrictions you set up.
Note that capability has a slightly different meaning here than for E's class certification. From the perspective of the E Environment, capability means an object's ability to either perform an action itself, or hand out another object that can perform the action. From the perspective of class certification, capability refers to a class that code (being loaded by the class loader) can access.
Using RtEEnvironment
To access the E environment classes and methods, you "launch" an object with RtLauncher's launch method. Launching an object gets it started, and gives the full set of RtEEnvironment capabilities (assuming the object's class has been granted the capability to access the classes in the RtEEnvironment tree). RtEEnvironment is the main template for the E Environment. Launching is demonstrated in the following procedure, Creating a restricted environment.
From the launched object, you can create a new environment with more limited capabilities, and pass it to other objects as needed. This ensures that these potentially untrusted objects only run with the restricted environment you created.
Specifying properties for the launched object
You can pass in arguments when you launch an object to set environment properties and arguments. You can pass in properties (those of x=y format, such as host=localhost), or arguments pertinent to your environment (arguments without "="). When you launch an object, those arguments are put into an environment dictionary.
You can pass in either arguments specific to that launched object, or the same ones that were entered at the command line. This lets you set the same environment properties for your launched object as you did for your main program.
For example, assume you run your program with a restricted path argument path=myDirectory/mySubDirectory entered as a command line argument. You can then pass this argument string to an object you launched; this object can then use the argument to restrict other objects to accessing only myDirectory/mySubDirectory.
In both Java and E, command line arguments are put into a standard arguments array. In E, when you launch an object, any argument of the form x=y is stripped out of the standard argument array, and put into a Properties hashtable, with x being the property key, and y being the property value. This makes it easy to specify properties on the command line, and then later retrieve specific ones to pass to environment objects. Arguments without "=" are retained in the standard argument string array.
NOTE: After you launch an object, E strips out any E-provided system arguments (the -EC* arguments) from the standard arguments array, since these are only pertinent to that execution of the program. See the description of these arguments in the E compiler documentation in Appendix A, E Tools and Utilities, for more information.
To access the arguments and property hash table, you use RtEEnvironment's getObjectFromDictionary method:
- To access the arguments array, run getObjectFromDictionary with the Args parameter:
env.getObjectFromDictionary("Args");To access the RtEEnvironment's properties, first create a hash table, then run RtEEnvironment.getObjectFromDictionary with the Properties parameter. For example:
Hashtable properties = (Hashtable); env.getObjectFromDictionary("Properties");Note that this might return null if there are no properties. To retrieve the properties from the hashtable you created, use the standard method for retrieving data from a hash table:
String value = (String) properties.get(String key);You can also get properties one at a time by passing in the property's key to the RtEEnvironment.getProperty method:
String value = env.getProperty(String key);This returns null if the property is not found, and the value RtEEnvironment.DefaultPropertyValue, if the property was specified as "key=" with no value.
Creating a restricted environment
The following annotated sample program demonstrates using RtEEnvironment to create restricted E application environments. This example restricts an object's file system access so it can only traverse a specific directory hierarchy.
Assume this example is run using the following line:
javaec ec.examples.file.FileExample path=tmp/examples -ECenableDgcThe path=tmp/examples argument is passed to the main object code. This argument is also passed to the launched EFileExampleLauncher object. The EFileExampleLauncher can then use this path in creating a restricted file system environment.
import java.lang.*; import java.io.*; import java.util.Hashtable; (1) import ec.e.comm.*; // // This is the boilerplate main that starts the whole // ball rolling. You launch something you trust and let // that make the determination of what it wants to hand // out to other objects. // public class FileExample { public static void main(String args[]) { if (args.length < 1) { System.out.println("Need to specify restriction dir path"); System.exit(0); } (2) RtLauncher.launch(new EFileExampleLauncher(), args); } } // // This is what is launched, and as stated before is something // you trust. It determines what to hand out to other entities. // In this (simple) case, it uses the directory handed to it // from the command line arguments (found in the tEEnvironment) // to determine the subdirectory it grants access to. // (3) eclass EFileExampleLauncher implements ELaunchable { emethod go (RtEEnvironment env) { // Get the args, and use arg[0] as the directory to // restrict to (4) String resPath = env.getProperty("path"); System.out.println("Restricting path to " + resPath); // Get the file environment and make the more restricted one (5) RtFileEnvironment fileEnv = env.getFileEnvironment(); (6) RtFileEnvironment restrictedFileEnv = fileEnv.restrictPath(resPath); // Create the entity we trust with this restricted // file environment and hand it the file env in a message (7) EFileExample theFileExample = new EFileExample(); theFileExample <- doStuff (restrictedFileEnv); } } // // This code would be loaded from somewhere else and // you would provide the restricted file environment to it. // (8) eclass EFileExample { emethod doStuff (RtFileEnvironment fileEnv) { try { String dirName = "/home/gordie/temp"; System.out.println("Dir Env rooted at " + fileEnv.getDirectory(dirName).getPath()); // This will return an object for temp in the // restricted file env RtFileDirectory dir = fileEnv.getDirectory(dirName); // This will return a file object for temp/filetest RtFile testFile = dir.getFile("filetest"); // etc.... } catch (IOException e) { System.out.println("Exception occurred: " + e.getMessage()); e.printStackTrace(); } } }
- This imports the comm package files necessary for creating an E environment, including RtEEnvironment and RtLauncher classes, plus the ELaunchable E-interface. ELaunchable contains the go method that your object will be sent to get it "started". This example also uses the RtFileEnvironment class to create restricted file environments.
- "Launch" a trusted object with RtEEnvironment's launch method. The object is launched with the command line string:
path=tmp/examplespassed in as an argument. This is put into a Properties hashtable, with path as its key and tmp/examples as its value. Note that the -ECenableDgc argument is stripped out and not passed in at launch time.
- This is the code for your launched class. This is a trusted class that determines what to hand out to other entities. In this example, it is passed the command line argument (path=tmp/examples). The launched object uses this path to determine the subdirectory it grants access to. The go method passes an RtEEnvironment object parameter so your class can access the RtEEnvironment facilities.
- The restricted path passed to the launched object (tmp/examples) is retrieved from the Properties hash table using the RtEEnvironment.getProperty method, specifying the keyword path. A string called resPath is set to this restricted path.
- Create an instance of RtFileEnvironment called fileEnv. The RtFileEnvironment class definition gives read/write access to the entire file system, which you can then restrict.
- Using the restricted path (resPath), as a parameter to RtFileEnvironment's restrictedFileEnv method, you create a more limited RtFileEnvironment that narrows file system access to resPath.
- Create an object with this restricted environment, and hand it the file environment in a message doStuff.
- This is the definition of the class to which you granted limited access. Typically, this would be in another file or package.
The E runtime network facilities
E provides classes for implementing network registration and connection. Use this functionality to register and connect E-objects over the network.
RtEEnvironment holds onto two capability objects:
- a connector, that encapsulates the right to establish outbound connections to other named (by URL) objects in the network.
- a registrar, which encapsulates the right to register objects by name in the local machine's object directory.
Both connectors and registrars restrict access by limiting the URL that objects can either look up on the network, or register themselves as.
You can limit the domain name, object pathname, port, or class.
The following example program demonstrates how to set up the registrar and connector.
Creating a registrar and connector
This program demonstrates how to create and connect a host and client object. The program is run first without a host name entered; this creates a host object which registers itself. The client then runs the program, using the host name as an argument, and connects to the host. The host can then shut down the environment.
import ec.e.comm.*; // // This is where it all starts, the main function // public class HelloExample { public static void main(String args[]) { (1) RtLauncher.launch(new HelloLauncher(), args); } } // // This object is totally trusted and makes subenvironments // that are passed to other less trusted objects. // eclass HelloLauncher implements ELaunchable { emethod go(RtEEnvironment env) { (2) RtNetworkController con; int defaultPortNumber = env.getDefaultPortNumber(); (3) String hostname = env.getProperty("host"); (4) if (hostname == null) { // We'll be the host HelloExampleHost host = new HelloExampleHost(); con = env.startNetworkEnvironment(defaultPortNumber); host <- goHost(env, con); } else { // We're the client, connect to host HelloExampleClient client = new HelloExampleClient(); con = env.startNetworkEnvironment(defaultPortNumber+1); client <- goClient(env, con, hostname); } } } (5) eclass HelloNetworkObject { static RtNetworkController networkController = null; emethod stopNetwork () { this.stopNetworkEnvironment(); } static void setNetworkController (RtNetworkController con) { networkController = con; } static void stopNetworkEnvironment () { if (networkController != null) networkController.stopNetworkEnvironment(); } } eclass HelloExampleClient extends HelloNetworkObject { emethod goClient (RtEEnvironment env, RtNetworkController con, String hostname) { HelloExampleHost host; this.setNetworkController(con); etry { System.out.println("Connecting to host"); (6) env.getConnector().lookupOnPort(hostname, "HelloExampleHost",RtComMonitor.keBasePortNumber, &host); } ecatch (RtDirectoryEException e) { System.out.println("Client caught exception: " + e.getMessage()); } host <- helloThere("This is a string", this); } } (7) eclass HelloExampleHost extends HelloNetworkObject { emethod goHost (RtEEnvironment env, RtNetworkController con) { this.setNetworkController(con); System.out.println("Registering host"); env.getRegistrar().register("HelloExampleHost", this); } (8) emethod helloThere (String theString, EObject sender) { System.out.println("Hello World"); if (sender != null) sender <- stopNetwork(); this <- stopNetwork(); } }
- A trusted HelloLauncher is launched, with the command line arguments passed in.
- A RtNetworkController called con is instantiated to handle the network environment for the host and client.
- A string called hostname is set to the value of the host entered at the command line. If none was entered, this is null.
- If no host was entered (hostname is null), the program assumes you are running as the host, and creates a HelloExampleHost called host. This object is sent the goHost method to register itself. If a host was entered, the program assumes you are running as the client, and creates a HelloExampleClient called client. This client is sent the goClient method to connect to the host.
- This is the template class for the network environment, which both the host and client extend. It contains methods to start and stop the network environment.
- The getConnector method lets the client connect to the host. If there is no host (for example, if you did not run the program to create a host first), it prints out an exception message.
- This is the code for the host object. It contains the getRegistrar method, which lets the host register itself on the network. The client then uses this registration information when trying to connect to the host.
- After the client connect to the host, it sends the host a helloThere message. The host accordingly prints out HelloWorld, then shuts down the network environment.
The E runtime file system facilities
The E runtime provides the following file system wrappers. These are described in more detail in the E Class Library chapter:
- RtFileEnvironment. This class encapsulates all of the file management functions, including getting, creating and deleting directories and files. The RtFileEnvironment that you get from the E Environment starts with unrestricted access to the entire file system that you can then restrict further to give to less trusted objects. You get files and objects by string names which return RtFileDirectory or RtFile objects.
- RtFileDirectory. This class lets you navigate and retrieve existing files and directories within its path.
- RtFile. This class lets you get a series of file objects related to a file. These objects can be read-only, write-only and read-write.
- RtDataInputFile, RtDataOutputFile, RtRandomAccessFile. These are runtime file primitives that wrap java.io.RandomFileAccess.
For an example of creating restricted file environments, see the sample program at the beginning of this section.
The E runtime environment provides a framework for cryptographic verification of the authorship and integrity of E classes and packages. In a networked world, users will want to download and run software from disparate sources on their machines. However, indiscriminate use of unverified software can enable the spread of viruses or Trojan horses that may damage or misappropriate user data. The E authentication framework enables the user to verify that:
- A class or package was in fact created by the entity claiming to be its author.
- A class or package has not been modified from the original version released by the author of record.
- No classes have been added to a package without the permission of the package's author.
- Classes and packages from unknown or untrusted sources are loaded only if they contain no references to capabilities the user has designated as restricted (such as disk or comm access).
- Only classes and packages from sources trusted by the user will be loaded. This provides the ability to control which classes are allowed to load which other classes.
Public-Key Cryptography
The basic enabling technology for E's trust management mechanism is a mathematical technique called public-key cryptography. This powerful technology enables private communication of data over public channels, even when the parties involved are not previously known to each other, and it enables unforgeable digital signatures that can be used to conclusively prove the origin of a message.
The security of conventional or secret-key cryptography relies on the secrecy of a key, or string of bits, used according to a specified recipe or algorithm to scramble the message being sent. Anyone who knows both the key and the algorithm can unscramble the message and retrieve the original (or plaintext) message. The secret key must be passed from one participant to the other, in some way immune to eavesdropping, before they can communicate.
In public-key cryptography (PKC), instead of a single secret key, one uses a pair of complementary keys, each of which can unscramble messages scrambled with the other. However, because of the mathematics underlying PKC, knowing one key of the pair does not make it practical to guess or calculate the other. A PKC user can make one key of the pair his public key, known to the world, and keep the other as his private key.
The fact that a public key can be used to decipher messages encoded with the corresponding private key enables the technique of digital signatures. A PKC user who wishes to sign some digital file attaches to it a digest -a summary of the file, generated by a well-defined one-way mathematical function or hash function-that is then encrypted with his private key. This encrypted digest, or digital signature, can be verified by anyone who knows the author's public key. They can generate a digest of the file using the same hash function, use the author's public key to decrypt the digital signature, and compare the two results. If the file has been altered since the author signed it, the two summaries will not match.
For more detailed information on this topic, see the excellent book Applied Cryptography (2d ed.) by Bruce Schneier.
Certificates and the Trust Boot File
E uses public-key cryptography and digital signatures to control the loading of classes and packages and to verify claims about their authorship and integrity. E allows classes to be loaded only if they are authenticated by trusted sources. The user can designate sources that are trusted by the system and can delegate that discredion to trusted certification authorities.
The security of the E runtime environment can be no more secure than the user's operating system and hard drive. The term trusted computing base or TCB refers to a computing environment that can be reasonably expected to be secure: the machine itself, its local storage media, its operating system, and the Java virtual machine binaries underlying E. E explicitly does not address issues arising from the compromise of any of these aspects of a trusted computing base.
Irrespective of the trustworthiness of the user's machine, the E runtime environment bases its trust management on a Trust Boot File (TBF) that resides on the user machine. In the TBF, the user specifies an initial set of statements regarding which users or code sources can be trusted, to what extent, and in what contexts. The user can also specify whether or not these trusted authorities may delegate trust, in the form of capabilities, to other entities.
A capability is the ability or authority to communicate with some object, device, or service. As far as a software object is concerned, the only other objects that exist in the universe are those to which it possesses a capability. The object is created with some set of capabilities; it can gain new capabilities only by receiving them from objects it already knows. The object cannot "make up" or "guess" a capability to a foreign object. ("Talking to strangers" is not possible.)
Trust, in the E environment, is represented by the digital signatures of trusted entities. The TBF contains certificates-well-formed assertions about keys, the entities or packages to which they correspond, and the kinds of claims those keys can credibly certify. The TBF also contains hints, which are URL-like constructs that indicate where to look for keys mentioned in the TBF and other forward-chained certificates.
The POLICY certificates in the TBF can be considered the axioms of a system of statements about trust. These axioms are used to generate theorems about the trustworthiness of other certificates, entities, keys, classes, and packages. Only classes whose trustworthiness is logically demonstrable within this system will be loaded and executed. This "predicate calculus of trust" has a semantics distinct from that of the E language.
Here is an example of what a certificate might look like:
FudcoKey = KeyFP(ECR, "AAACAAAAAEDMGRQy4svezmyBOzQEwpjhuYX RKC0pDqA4ha89oyuBqLI1ghcCoww1vdj/krFq97HvSOFMtf9/8bB8u SdqMZE1AAAAAwEAAaA4"); FudcoDevelop = KeyFP(ECR, "FAAFAAFAA"); POLICY states { FudcoKey CanMakeClaim where ( delegate == "true" and claim == "PkgBelongCert where (pkg == \"*\")" // BUG: claim == "*" breaks here ); FudcoDevelop CanMakeClaim where ( delegate == "yes" and claim == "PkgBelongCert where (pkg == \"fudco.*\")" ); } ## ## Forward: "http://fudco.com/keys/mis-boot-keys" Resolve: "http://keys1.fudco.com/keys/keyserve/" Resolve: "http://keys2.fudco.com/keys/keyserve/"
Assignment Statements
The first two lines of this example,
FudcoKey = Key(ECR, "AAACAAAAAEDMGRQy4svezmyBOzQEwpjhuYX> RKC0pDqA4ha89oyuBqLI1ghcCoww1vdj/krFq97HvSOFMtf9/8bB8u SdqMZE1AAAAAwEAAaA4"); FudcoDevelop = KeyFP(ECR, "FAAFAAFAA");are assignment statements that bind variables to keys. These variables are local to the certificate; subsequent statements in the certificate can then refer to each key by the assigned name. Bindings are of the form:
NAME = Key(key_type, key_value); | NAME = KeyFP(key_type, keyfp_value); | NAME = KeyID(key_type, keyid_value);The assignment statement can either provide the entire public key itself (as in the FudcoKey example above), a fingerprint of the key, or an ID for the key. A key's fingerprint is a digital digest or hash of the key's full text that can be used to refer to the key uniquely. The key's ID is a name, defined by some third party, by which the key can be looked up (as, for example, in a "keyring" file on the user's disk, or a public "White Pages" directory of public keys).
The key_type tells the Trust Manager how to interpret the referenced key.
NOTE: The present release supports keys of type ECR; future releases will support additional standard key types.
Source Claim Statements
A source claim statement in a certificate asserts a rule for deciding the trustworthiness of keys, classes, or packages. The source of the claim may be POLICY, which indicates that the claim is assumed by the user to be true without proof, or the claim may be signed by a key. If the source of the claim is a key, it must be identified precisely so the source can be checked at runtime.
Claims are of three types:
- The source delegates some or all of its authority to another source (CanMakeClaim).
- The source asserts that a particular class belongs to a particular package (PkgBelongCert).
- The source declares that some class A is allowed to load another (potentially dangerous) class B (LoadOkayCert).
CanMakeClaim allows formulation of claims about claims; a trusted source can define what types of claims another source can credibly make.
For example, a user at FudCo might have the following in their TBF, where FudCoKey is a key belonging to FudCo's MIS department and FudCoDevelop is a key belonging to FudCo's R & D department:
FudCoKey = KeyFP(ECR,"ABCABC"); FudcoDevelop = KeyFP(ECR,"DEFDEF"); POLICY states { FudCoKey CanMakeClaim where ( delegate == "yes" and claim == "*" ); FudcoDevelop CanMakeClaim where ( delegate == "yes" and claim == "PkgBelongCert where (pkg == \"fudco.*\")" } FudcoDevelop CanMakeClaim where ( delegate == "yes" and claim == "LoadOkayCert where ()" } }The first claim statement
FudCoKey CanMakeClaim where ( delegate == "yes" and claim == "*" )means "trust any claim made by FudCoKey about other keys." FudCoKey is allowed to delegate all of its authority to other keys. FudCo's MIS department can then use FudCoKey to set company-wide policy on what classes to allow or disallow. The next statement
FudcoDevelop CanMakeClaim where ( delegate == "yes" and claim == "PkgBelongCert where (pkg == \"fudco.*\")" }signifies that any claim signed by the FudCoDevelop key regarding class-package membership is to be trusted. The FudCoDevelop key might be used by FudCo's software R & D division to sign work-in-progress for use internally.
Finally,
FudcoDevelop CanMakeClaim where ( delegate == "yes" and claim == "LoadOkayCert where ()" }means that the holders of the FudCoDevelop key can specify what classes are allowed to load what other classes, and can delegate that authority to other keys.
The LoadOkayCert predicate is used to restrict the ability of random classes to load potentially dangerous classes. For example, FileInputStream.class is used to read files from local storage; an E application able to load this class could potentially compromise user data. To extend this capability to some other class, some certificate would state:
MySysopKey states { FISKey CanMakeClaim where (delegate == "no" and claim == "LoadOkayCert" where (loaded_class == \"FileInputStream.class\" and loaded_hash == \"00C62F0D2F104EBA\" ) " ) }where MySysopKey is authorized to delegate such authority. FISKey can then make LoadOkayCert claims of the specified kind, like so:
FISKey states { LoadOkayCert where (loading_class == "Fud.class" and loading_hash == "20502E182C10A9A3" and loaded_class == "FileInputStream.class" and loaded_hash == "00C62F0D2F104EBA" ) }Hints
These are URLs where the Trust Manager is instructed to look for keys, certificates, and packages mentioned in the TBF. Hints are used to resolve, for example, the actual value of keys identified by their ID or fingerprint; the Trust Manager queries keyservers at these URLs to find the key that matches the fingerprint. For example, Sun might publish a key to verify the integrity of classes belonging to the Java distribution.
SunMasterKey = KeyFP(ECR, "10C0B1CD6DFA41ED"); FudcoMISKey = KeyFP(ECR, "A9A0205720086700"); FudcoMISKey states { SunMasterKey CanMakeClaim where (delegate == "yes" and claim == "PkgBelongCert where (pkg == \"sun.*\" or pkg == \"java.*\")" ); } Resolve: "http://keys.javasoft.com/keyserve/"The Resolve: hint tells the trust manager where to look for an appropriate SunMasterKey that fits the specified fingerprint. Hints may also be used to tell the Trust Manager where to look for more certificates or classes signed by keys it has just been instructed to trust:
Forward: "http://fudco.com/keys/mis-boot-keys" /* a repository where FudCo's MIS department maintains an up-to-date archive of trust policies */Auxiliary Data
A certificate may contain other information such as generation and expiration dates, version numbers, and so on.
Signatures
A signed certificate contains a digital signature, generated by encrypting a digest of the other portions of the certificate. The Trust Manager can use this signature to verify that the certificate has not been tampered with. POLICY certificates are unsigned because they are assumed to be set by the user; their trustworthiness depends on the physical security of the user machine.