The Artima Developer Community
Sponsored Link

Chapter 3 of Inside the Java Virtual Machine
Security
by Bill Venners

Aside from platform independence, discussed in the previous chapter, the other major technical challenge a network-oriented software technology must deal with is security. Networks, because they allow computers to share data and distribute processing, can potentially serve as a way to break into a computer system, enabling someone to steal information, alter or destroy information, or steal computing resources. As a consequence, connecting a computer to a network raises many security issues.

To address the security concerns raised by networks, Java's architecture comes with an extensive built- in security model, which has evolved with each major release of the Java Platform. This chapter gives an overview of the security model built into Java's core architecture and traces its evolution.

Why Security?

Java's security model is one of the key architectural features that makes it an appropriate technology for networked environments. Security is important because networks represent a potential avenue of attack to any computer hooked to them. This concern becomes especially strong in an environment in which software is downloaded across the network and executed locally, as is done, for example, with Java applets and Jini service objects. Because the class files for an applet are automatically downloaded when a user goes to the containing web page in a browser, it is likely that a user will encounter applets from untrusted sources. Similarly, the class files for a Jini service object are downloaded from a code base specified by the service provider when it registers its service with the Jini lookup service. Because Jini enables spontaneous networking in which users entering a new environment look up and access locally available services, users of Jini services will likely encounter service objects from untrusted sources. Without any security, these automatic code download schemes would be a convenient way to distribute malicious code. Thus, Java's security mechanisms help make Java suitable for networks because they establish a needed trust in the safety of executing network-mobile code.

Java's security model is focused on protecting end-users from hostile programs (and bugs in otherwise benevolent programs) downloaded across a network from untrusted sources. To accomplish this goal, Java provides a customizable "sandbox" in which untrusted Java programs can be placed. The sandbox restricts the activities of the untrusted program. The program can do anything within the boundaries of its sandbox, but can't take any action outside those boundaries. For example, the original sandbox for untrusted Java applets in version 1.0 prohibited many activities, including:

By making it impossible for downloaded code to perform certain actions, Java's security model protects the end-user from the threats of hostile and buggy code.

Because the sandbox security model imposes strict controls on what untrusted code can and cannot do, users are able to run untrusted code with relative safety. Unfortunately for the programmers and users of 1.0 systems, however, the original sandbox was so restrictive, that well-meaning (but untrusted) code was often unable to do useful work. In version 1.1, the original sandbox model was augmented with a trust model based on code signing and authentication. The signing and authentication capability enables the receiving system to verify that a set of class files (in a JAR file) has been digitally signed (in effect, blessed as trustworthy) by some entity, and that the class files have not been altered since they were signed. This enables end users and system administrators to ease the restrictions of the sandbox for code that has been digitally signed by trusted parties.

Although the security APIs released with version 1.1 include support for authentication, they don't offer much help in establishing anything more than an all-or-nothing trust policy (in other words, either code is completely trusted or completely untrusted). Java's next major release, version 1.2, provided APIs to assist in establishing fine-grained security policies based on authentication of digitally signed code. The remainder of this chapter will trace the evolution of Java's security model from the basic sandbox of 1.0, through the code signing and authentication of 1.1, to the fine-grained access control of 1.2.

The Basic Sandbox

In the world of personal computers, you have traditionally had to trust software before you ran it. You achieved security by being careful only to use software from trusted sources, and by regularly scanning for viruses just to make sure. Once some software got access to your system, it had full reign. If it was malicious, it could do a great deal of damage because there were no restrictions placed on it by the runtime environment of your computer. So in the traditional security scheme, you tried to prevent malicious code from ever gaining access to your computer in the first place.

The sandbox security model makes it easier to work with software that comes from sources you don't fully trust. Instead of approaching security by requiring you to prevent any code you don't trust from ever making its way onto your computer, the sandbox model allows you to welcome code from any source. But as code from an untrusted source runs, the sandbox restricts the code from taking any actions that could possibly harm your system. You don't need to figure out what code you can and can't trust. You don't need to scan for viruses. The sandbox itself prevents any viruses or other malicious or buggy code you may invite into your computer from doing any damage.

If you have a properly skeptical mind, you'll need to be convinced a sandbox has no leaks before you trust it to protect you. To make sure the sandbox has no leaks, Java's security model involves every aspect of its architecture. If there were areas in Java's architecture where security was not considered, a malicious programmer (a "cracker") could likely exploit those areas to "go around" the sandbox. To understand the sandbox, therefore, you must look at several different parts of Java's architecture, and understand how they work together.

The fundamental components responsible for Java's sandbox are:

One of the greatest strengths of Java's sandbox security model is that two of these components, the class loader and security manager, are customizable. By customizing these components, you can create a customized security policy for a Java application. Unfortunately, this customizability doesn't come for free, because the very flexibility of the architecture creates some risks of its own. Class loaders and security managers are complicated enough that the mere act of customization can potentially produce errors that open up security holes.

In each major release of the Java API, changes were made to make the task of creating a custom security policy less prone to error. The most significant change occurred in version 1.2, which introduced a new and more elaborate architecture for access control. In version 1.0 and 1.1, access control, which involves both the specification of a security policy and the enforcement of that policy at run time, is the responsibility of an object called the security manager. To establish a custom policy in 1.0 and 1.1, you have to write your own custom security manager. In 1.2, you can take advantage of a security manager supplied with the Java 2 Platform. This ready made security manager allows you to specify a security policy in an ASCII policy file separate from the program. At runtime, the ready made security manager enlists the help of a class called the access controller to enforce the security policy specified in the policy file. The access control infrastructure introduced in 1.2 provides a flexible and easily customized default implementation of the security manager that should suffice for the majority of security needs. For backwards compatibility, and to enable parties with special security needs to override the default functionality provided by the ready made security manager, version 1.2 applications can still install their own security manager. Using the ready made made security manager, and the extensive access control infrastructure that comes with it, is optional.

The Class Loader Architecture

In Java's sandbox, the class loader architecture is the first line of defense. It is the class loader, after all, that brings code into the Java virtual machine--code that could be hostile or buggy. The class loader architecture contributes to Java's sandbox in three ways:

  1. it prevents malicious code from interfering with benevolent code,
  2. it guards the borders of the trusted class libraries, and
  3. it places code into categories (called protection domains) that will determine which actions the code will be allowed to take.

The class loader architecture prevents malicious code from interfering with benevolent code by providing separate name-spaces for classes loaded by different class loaders. A name-space is a set of unique names -- one name for each loaded class -- that the Java virtual machine maintains for each class loader. Once a Java virtual machine has loaded a class named Volcano into a particular name-space, for example, it is impossible to load a different class named Volcano into that same name-space. You can load multiple Volcano classes into a Java virtual machine, however, because you can create multiple name-spaces inside a Java application by creating multiple class loaders. If you create three separate name-spaces (one for each of three class loaders) in a running Java application, then, by loading one Volcano class into each name-space, your program could load three different Volcano classes into your application.

Name-spaces contribute to security because you can in effect place a shield between classes loaded into different name-spaces. Inside the Java virtual machine, classes in the same name-space can interact with one another directly. Classes in different name-spaces, however, can't even detect each other's presence unless you explicitly provide a mechanism that allows them to interact. If a malicious class, once loaded, had guaranteed access to every other class currently loaded by the virtual machine, that class could potentially learn things it shouldn't know or interfere with the proper execution of your program.

Figure 3-1 shows the name-spaces associated with two class loaders, both of which have loaded a type named Volcano. Each name in a name space is associated with the type data in the method area that defines the type with that name. Figure 3-1 shows arrows from the names in the name- spaces to the types in the method area that define the type. The class loader on the left, which is shown dark gray, has loaded the two dark gray types named Climber and Volcano. They class loader on the right, which is shown light gray, has loaded the two light gray types named BakingSoda and Volcano. Because of the nature of name-spaces, when the Climber class mentions the Volcano class, it refers to the dark gray Volcano, the Volcano loaded in the same name space. It has no way to know that the other Volcano, which is sitting in the same virtual machine, even exists. For the details on how the class loader architecture achieves its separation of namespaces, see Chapter 8, "The Linking Model."



Figure 3-1. Class loaders and name-spaces.

The class loader architecture guards the borders of the trusted class libraries by making it possible for trusted packages to be loaded with different class loaders than untrusted packages. Although you can grant special access privileges between types belonging to the same package by giving members protected or package access, this special access is granted to members of the same package at runtime only if they were loaded by the same class loader.

Often, a user-defined class loader relies on other class loaders--at the very least, upon the class loaders created at virtual machine startup--to help it fulfill some of the class load requests that come its way. Prior to 1.2, class loaders had to explicitly ask for the help of other class loaders. A class loader could ask another user-defined class loader to load a class by invoking loadClass() on a reference to that user-defined class loader. Or, a class loader could ask the bootstrap class loader to attempt to load a class by invoking findSystemClass(), a static method defined in class ClassLoader. In version 1.2, the process by which one class loader asks another class loader to try and load a type was formalized into a parent-delegation model. Starting with 1.2, each class loader except the bootstrap class loader has a "parent" class loader. Before a particular class loader attempts to load a type in its custom way, it by default "delegates" the job to its parent -- it asks its parent to try and load the type. The parent, in turn, asks its parent to try and load the type. The delegation process continues all the way up to the bootstrap class loader, which is in general the last class loader in the delegation chain. If a class loader's parent class loader is able to load a type, the class loader returns that type. Else, the class loader attempts to load the type itself.

In most Java virtual machine implementations prior to 1.2, the built-in class loader (which was then called the primordial class loader) was responsible for loading locally available class files. Such class files usually included the class files that made up the Java application being executed plus any libraries needed by the application, including the class files of the Java API. Although the manner in which the class files for requested types were located was implementation specific, many implementations searched directories and JAR files in an order specified by a class path.

In 1.2, the job of loading locally available class files was parceled out to multiple class loaders. The built-in class loader, previously called the primordial class loader, was renamed the "bootstrap" class loader to indicate that it was now responsible for loading only the class files of the core Java API. The name bootstrap class loader comes from the idea that the class files of the core Java API are the class files required to "bootstrap" the Java virtual machine.

Responsibility for loading other class files, such as the class files for the application being executed, class files for installed or downloaded standard extensions, class files for libraries discovered in the class path, and so on, was given in 1.2 to user-defined class loaders. When a 1.2 Java virtual machine starts execution, therefore, it creates at least one and probably more user-defined class loaders before the application even starts. All of these class loaders are connected in one chain of parent-child relationships. At the top of the chain is the bootstrap class loader. At the bottom of the chain is what came in 1.2 to be called the "system class loader." Prior to 1.2, the name "system class loader" was sometimes used to refer to the built-in class loader, which was also called the primordial class loader. In 1.2, the name system class loader was more formally defined to mean the default delegation parent for new user-defined class loaders created by a Java application. This default delegation parent is usually going to be the user-defined class loader that loaded the initial class of the application, but may be any user-defined class loader decided upon by the designers of the Java Platform implementation.

For example, imagine you write a Java application that installs a class loader whose particular manner of loading class files is by downloading them across a network. Imagine you run this application on a virtual machine that instantiates two user-defined class loaders on startup: an "installed extensions" class loader and a "class path" class loader. These class loaders are connected in a parent-child relationship chain along with the bootstrap class loader as shown in Figure 3-2. The class path's class loader's parent is the installed extensions class loader, whose parent is the bootstrap class loader. As shown in Figure 3-2, the class path class loader is designated as the system class loader, the default delegation parent for new user-defined class loaders instantiated by the application. Assume that when you application instantiates its network class loader, it specifies the system class loader as its parent.



Figure 3-2. A parent-child class loader delegation chain.

Imagine that during the course of running the Java application, a request is made of your class loader to load a class named Volcano. Your class loader would first ask its parent, the class path class loader, to find and load the class. The class path class loader, in turn, would make the same request of its parent, the installed extensions class loader. This class loader, would also first delegate the request to its parent, the bootstrap class loader. Assuming that class Volcano is not a part of the Java API, an installed extension, or on the class path, all of these class loaders would return without supplying a loaded class named Volcano. When the class path class loader responds that neither it nor any of its parents can load the class, your class loader could then attempt to load the Volcano class in its custom manner, by downloading it across the network. Assuming your class loader was able to download class Volcano, that Volcano class could then play a role in the application's future course of execution.

To continue with the same example, assume that at some time later a method of class Volcano is invoked for the first time, and that method references class java.util.HashMap from the Java API. Because it is the first time the reference was used by the running program, the virtual machine asks your class loader (the one that loaded Volcano) to load java.util.HashMap. As before, your class loader first passes the request to its parent class loader, and the request gets delegated all the way up to the bootstrap class loader. But in this case, the bootstrap class loader is able to return a java.util.Hashmap class back to your class loader. Since the bootstrap class loader was able to find the class, the installed extensions class loader doesn't attempt to look for the type in the installed extensions. The class path class loader doesn't attempt to look for the type on the class path. And your class loader doesn't attempt to download it across the network. All of these class loaders merely return the java.util.HashMap class returned by the bootstrap class loader. From that point forward, the virtual machine uses that java.util.HashMap class whenever class Volcano references a class named java.util.HashMap.

Given this background into how class loaders work, you are ready to look at how class loaders can be used to protect trusted libraries. The class loader architecture guards the borders of the trusted class libraries by preventing untrusted classes from pretending to be trusted. If a malicious class could successfully trick the Java virtual machine into believing it was a trusted class from the Java API, that malicious class could potentially break through the sandbox barrier. By preventing untrusted classes from impersonating trusted classes, the class loader architecture blocks one potential approach to compromising the security of the Java runtime.

Given the parent-delegation model, the bootstrap class loader is able to attempt to load types before the standard extensions class loader, which is able to attempt to load types before the class path class loader, which is able to attempt to load types before your network class loader. Thus, given the manner in which the parent-child delegation chain is built, the most trusted library, the core Java API, is checked first for each type. After that, the standard extensions are checked. After that, local class files that are sitting on the class path are checked. So if some mobile code loaded by your network class loader wants to download a type across the network with the same name as something in the Java API, such as java.lang.Integer, it won't be able to do it. If a class file for java.lang.Integer exists in the Java API, it will be loaded by the bootstrap class loader. The network class loader will not attempt to download and define a class named java.lang.Integer. It will just use the type returned by its parent, the one loaded by the bootstrap class loader. In this way, the class loader architecture prevents untrusted code from replacing trusted classes with their own versions.

But what if the mobile code, rather than trying to replace a trusted type, wants to insert a brand new type into a trusted package? Imagine what would happen if your network class loader from the previous example was requested to load a class named java.lang.Virus. As before, this request would first be delegated all the way up the parent-child chain to the bootstrap class loader. Although the bootstrap class loader is responsible for loading the class files of the core Java API, which includes a package named, java.lang, it is unable to find a member of the java.lang package with the name, Virus. Assuming this class was also not found among the installed extensions or on the local class path, your class loader would proceed to attempt to download the type across the network.

Assume your class loader is successful in the download attempt and defines the type named java.lang.Virus. Java allows classes in the same package to grant each other special access privileges that aren't granted to classes outside the package. So, since your class loader has loaded a class that by its name (java.lang.Virus) brazenly declares itself to be part of the Java API, you might expect it could gain special access to the trusted classes of java.lang and could possibly use that special access for devious purposes. The class loader mechanism thwarts this code from gaining special access to the trusted types in the java.lang package, because the Java virtual machine only grants that special package access between types loaded into the same package by the same class loader. Since the trusted class files of the Java API's java.lang package were loaded by the bootstrap class loader, and the malicious java.lang.Virus class was loaded by your network class loader, these types do not belong to the same runtime package. The term runtime package, which first appeared in the second edition of the Java Virtual Machine Specification, refers to a set of types that belong to the same package and that were all loaded by the same class loader. Before allowing access to package- visible members (members declared with protected or package access) between two types, the virtual machine makes sure not only that the two types belong to the same package, but that they belong to the same runtime package -- that they were loaded by the same class loader. Thus, because java.lang.Virus and the members of java.lang from the core Java API don't belong to the same runtime package, java.lang.Virus can't access the package-visible members and types of the Java API's java.lang package.

This concept of a runtime package is one motivation for using different class loaders to load different kinds of classes. The bootstrap class loader loads the class files of the core Java API. These class files are the most trusted. An installed extensions class loader loads class files from any installed extensions. Installed extensions are quite trusted, but need not be trusted to the extent that they are allowed to gain access to package-visible members of the Java API by simply inserting new types into those packages. Because installed extensions are loaded with a different class loader than the core API, they can't. Likewise, code found on the class path by the class path class loader can't gain access to package-visible members of the installed extensions or the Java API.

Another way class loaders can be used to protect the borders of trusted class libraries is by simply disallowing the loading of certain forbidden types. For example, you may have installed some packages that contain classes you want your application to be able to load through your network class loader's parent, the class path class loader, but not through your own network class loader. Assume you have created a package named absolutepower and installed it somewhere on the local class path, where it is accessible by the class path class loader. Assume also that you don't want classes loaded by your class loader to be able to load any class from the absolutepower package. In this case, you would write your class loader such that the very first thing it does is make sure the requested class doesn't declare itself as a member of the absolutepower package. If such a class is requested, your class loader, rather than passing the class name to its parent class loader, would throw a security exception.

The only way a class loader can know whether or not a class is from a forbidden package, such as absolutepower, is by the class's name. Thus a class loader must have a list of the names of forbidden packages. Because the name of class absolutepower.FancyClassLoader indicates it is part of the absolutepower package, and the absolutepower package is on the list of forbidden packages, your class loader should throw a security exception absolutely.

Besides shielding classes in different namespaces and protecting the borders of trusted class libraries, class loaders play one other security role. Class loaders must place each loaded class into a protection domain, which defines what permissions the code is going to be given as it runs. More information about this vitally important security job of class loaders will be given later in this chapter.

The Class File Verifier

Working in conjunction with the class loader, the class file verifier ensures that loaded class files have a proper internal structure and that they are consistent with each other. If the class file verifier discovers a problem with a class file, it throws an exception. Although compliant Java compilers should not generate malformed class files, a Java virtual machine can't tell how a particular class file was created. Because a class file is just a sequence of bytes, a virtual machine can't know whether a particular class file was generated by a well-meaning Java compiler or by shady crackers bent on compromising the integrity of the virtual machine. As a consequence, all Java virtual machine implementations have a class file verifier that can be invoked on class files, to make sure the types they define are safe to use.

One of the security goals that the class file verifier helps achieve is program robustness. If a buggy compiler or savvy cracker generated a class file that contained a method whose bytecodes included an instruction to jump beyond the end of the method, that method could, if it were invoked, cause the virtual machine to crash. Thus, for the sake of robustness, it is important that the virtual machine verify the integrity of the bytecodes it imports.

The class file verifier of the Java virtual machine does most checking before bytecodes are executed. Rather than checking every time it encounters a jump instruction as it executes bytecodes, for example, it analyzes bytecodes (and verifies their integrity) once, before they are ever executed. As part of its verification of bytecodes, the Java virtual machine makes sure all jump instructions cause a jump to another valid instruction in the bytecode stream of the method. In most cases, checking all bytecodes once, before they are executed, is a more efficient way to guarantee robustness than checking every bytecode instruction every time it is executed.

The class file verifier operates in four distinct passes. During pass one, which takes place as a class is loaded, the class file verifier checks the internal structure of the class file to make sure it is safe to parse. During passes two and three, which take place during linking, the class file verifier makes sure the type data obeys the semantics of the Java programming language, including verifying the integrity of any bytecodes it contains. During pass four, which takes place as symbolic references are resolved in the process of dynamic linking, the class file verifier confirms the existence of symbolically referenced classes, fields, and methods.

Pass One: Structural Checks on the Class File

During pass one, the class file verifier makes certain that the sequence of bytes it is about to attempt to import as a type conforms to the basic structure of a Java class file. The verifier performs many checks during this pass. For example, every class file must start with the same four bytes, the magic number: 0xCAFEBABE. The purpose of the magic number is to make it easy for the class file parser to reject files that were either damaged or were never intended to be class files in the first place. Thus, the first thing a class file verifier likely checks is that the imported file does indeed begin with 0xCAFEBABE. The verifier also makes sure the major and minor version numbers declared in the class file are within the range supported by that implementation of the Java virtual machine.

Also during pass one, the class file verifier checks to make sure the class file is neither truncated nor enhanced with extra trailing bytes. Although different class files can be different lengths, each individual component contained inside a class file indicates its length as well as its type. The verifier can use the component types and lengths to determine the correct total length for each individual class file. In this way, it can verify that the imported file has a length consistent with its internal contents.

The point of pass one is to ensure the sequence of bytes that supposedly define a new type adheres sufficiently to the Java class file format to enable it to be parsed into implementation-specific internal data structures in the method area. Passes two, three, and four take place not on the binary data in the class file format, but on the implementation-specific data structures in the method area.

Pass Two: Semantic Checks on the Type Data

Pass two of the class file verifier performs checking that can be done without looking at the bytecodes and without looking at (or loading) any other types. During this pass, the verifier looks at individual components, to make sure they are well-formed instances of their type of component. For example, a method descriptor (its return type and the number and types of its parameters) is stored in the class file as a string that must adhere to a certain context-free grammar. One check the verifier performs on individual components is to make sure each method descriptor is a well-formed string of the appropriate grammar.

In addition, the class file verifier checks that the class itself adheres to certain constraints placed upon it by the specification of the Java programming language. For example, the verifier enforces the rule that all classes, except class Object, must have a superclass. Also during pass two, the verifier makes sure that final classes are not subclassed and final methods are not overridden. In addition, it checks that constant pool entries are valid, and that all indexes into the constant pool refer to the correct type of constant pool entry. Thus, the class file verifier checks at run-time some of the Java language rules that should have been enforced at compile-time. Because the verifier has no way of knowing if the class file was generated by a benevolent, bug-free compiler, it checks each class file to make sure the rules are followed.

Pass Three: Bytecode Verification

Once the class file verifier has successfully completed the pass two checks, it turns its attention to the bytecodes. During this pass, which is commonly called the "bytecode verifier," the Java virtual machine performs a data-flow analysis on the streams of bytecodes that represent the methods of the class. To understand the bytecode verifier, you need to understand a bit about bytecodes and frames.

The bytecode streams that represent Java methods are a series of one-byte instructions, called opcodes, each of which may be followed by one or more operands. The operands supply extra data needed by the Java virtual machine to execute the opcode instruction. The activity of executing bytecodes, one opcode after another, constitutes a thread of execution inside the Java virtual machine. Each thread is awarded its own Java Stack, which is made up of discrete frames. Each method invocation gets its own frame, a section of memory where it stores, among other things, local variables and intermediate results of computation. The part of the frame in which a method stores intermediate results is called the method's operand stack. An opcode and its (optional) operands may refer to the data stored on the operand stack or in the local variables of the method's frame. Thus, the virtual machine may use data on the operand stack, in the local variables, or both, in addition to any data stored as operands following an opcode when it executes the opcode.

The bytecode verifier does a great deal of checking. It checks to make sure that no matter what path of execution is taken to get to a certain opcode in the bytecode stream, the operand stack always contains the same number and types of items. It checks to make sure no local variable is accessed before it is known to contain a proper value. It checks that fields of the class are always assigned values of the proper type, and that methods of the class are always invoked with the correct number and types of arguments. The bytecode verifier also checks to make sure that each opcode is valid, that each opcode has valid operands, and that for each opcode, values of the proper type are in the local variables and on the operand stack. These are just a few of the many checks performed by the bytecode verifier, which is able, through all its checking, to verify that a stream of bytecodes is safe for the Java virtual machine to execute.

The bytecode verifier doesn't attempt to detect all safe programs. If it tried to do that, it would run up against the Halting Problem. The Halting Problem, a well-known theorem in computer science, states that you can't write a program that can determine whether any program fed to it as input will halt when it is executed. Whether or not a program will halt is called an "undecidable" property of the program, because you can't write a program that can tell you 100% of the time whether or not any given program has the property. The undecideability of the Halting Problem extends to many properties of computer programs, including whether or not a set of Java bytecodes would be safe for a Java virtual machine to execute.

The way the bytecode verifier gets around the Halting Problem is by not attempting to pass all safe programs. Although you can't write a program that can determine whether or not any given program will halt, you can write a program that recognizes some programs that will halt. For example, if the first instruction of a program is halt, that program will halt. If a program has no loops in it, it will halt, and so on. Similarly, although you can't write a verifier that will pass all bytecode streams that are safe for the virtual machine to execute, you can write a verifier that will pass some of them. And that's just what Java's bytecode verifier does. The verifier checks to make sure a certain set of rules are followed by each set of bytecodes its fed. If a set of bytecodes obeys all the rules, the verifier knows the bytecodes are safe for the virtual machine to execute. If not, the bytecodes may or may not be safe for the virtual machine to execute. Thus, the verifier gets around the Halting Problem by recognizing some, but not all, safe bytecode streams. Given the nature of the constraints checked by the bytecode verifier, any program that can be written in the Java programming language can be compiled to bytecodes that will pass the verifier. Some programs that could not possibly be expressed in the Java programming language will pass the verifier. And some programs (also not expressible in Java source code) that would otherwise be safe for the virtual machine to execute, will not pass the verifier.

Passes one, two, and three of the class file verifier make sure the imported class file is properly formed, internally consistent, adheres to the constraints of the Java programming language, and contains bytecodes that will be safe for the Java virtual machine to execute. If the class file verifier finds that any of these are not true, it throws an error, and the class file is never used by the program.

Pass Four: Verification of Symbolic References

Pass four of the class file verifier takes place when the symbolic references contained in a class file are resolved in the process of dynamic linking. During pass four, the Java virtual machine follows the references from the class file being verified to the referenced class files, to make sure the references are correct. Because pass four has to look at other classes external to the class file being checked, pass four may require that new classes be loaded. Most Java virtual machine implementations will likely delay loading classes until they are actually used by the program. If an implementation does load classes earlier, perhaps in an attempt to speed up the loading process, then it must still give the impression that it is loading classes as late as possible. If, for example, a Java virtual machine discovers during early loading that it can't find a certain referenced class, it doesn't throw a NoClassDefFoundError error until (and unless) the referenced class is used for the first time by the running program. Thus, if a Java virtual machine performs early linking, pass four could happen shortly after pass three. But in Java virtual machines that resolve each symbolic reference the first time it is used, pass four will happen much later than pass three, as bytecodes are executed.

Pass four of class file verification is really just part of the process of dynamic linking. When a class file is loaded, it contains symbolic references to other classes and their fields and methods. A symbolic reference is a character string that gives the name and possibly other information about the referenced item- -enough information to uniquely identify a class, field, or method. Thus, symbolic references to other classes give the full name of the class; symbolic references to the fields of other classes give the class name, field name, and field descriptor; symbolic references to the methods of other classes give the class name, method name, and method descriptor.

Dynamic linking is the process of resolving symbolic references into direct references. As the Java virtual machine executes bytecodes and encounters an opcode that, for the first time, uses a symbolic reference to another class, the virtual machine must resolve the symbolic reference. The virtual machine performs two basic tasks during resolution:

  1. finds the class being referenced (loading it if necessary)
  2. replaces the symbolic reference with a direct reference, such as a pointer or offset, to the class, field, or method
The virtual machine remembers the direct reference so that if it encounters the same reference again later, it can immediately use the direct reference without needing to spend time resolving the symbolic reference again.

When the Java virtual machine resolves a symbolic reference, pass four of the class file verifier makes sure the reference is valid. If the reference is not valid--for instance, if the class cannot be loaded or if the class exists but doesn't contain the referenced field or method--the class file verifier throws an error.

As an example, consider again the Volcano class. If a method of class Volcano invokes a method in a class named Lava, the name and descriptor of the method in Lava are included as part of the binary data in the class file for Volcano. When Volcano's method first invokes Lava's method during the course of execution, the Java virtual machine makes sure a method exists in class Lava that has a name and descriptor that matches those expected by class Volcano. If the symbolic reference (class name, method name and descriptor) is correct, the virtual machine replaces it with a direct reference, such as a pointer, which it will use from then on. But if the symbolic reference from class Volcano doesn't match any method in class Lava, pass four verification fails, and the Java virtual machine throws a NoSuchMethodError.

Binary Compatibility

The reason pass four of the class file verifier must look at classes that refer to one another to make sure they are compatible is because Java programs are dynamically linked. Java compilers will often recompile classes that depend on a class you have changed, and in so doing, detect any incompatibility at compile- time. But there may be times when your compiler doesn't recompile a dependent class. For example, if you are developing a large system, you will likely partition the various parts of the system into packages. If you compile each package separately, then a change to one class in a package would likely cause a recompilation of affected classes within that same package, but not necessarily in any other package. Moreover, if you are using someone else's packages, especially if your program downloads class files from someone else's package across a network as it runs, it may be impossible for you to check for compatibility at compile-time. That's why pass four of the class file verifier must check for compatibility at run-time.

As an example of incompatible changes, imagine you compiled class Volcano (from the previous example) with a Java compiler. Because a method in Volcano invokes a method in another class named Lava, the Java compiler would look for a class file or a source file for class Lava to make sure there was a method in Lava with the appropriate name, return type, and number and types of arguments. If the compiler couldn't find any Lava class, or if it encountered a Lava class that didn't contain the desired method, the compiler would generate an error and would not create a class file for Volcano. Otherwise, the Java compiler would produce a class file for Volcano that is compatible with the class file for Lava. In this case, the Java compiler refused to generate a class file for Volcano that wasn't already compatible with class Lava.

The converse, however, is not necessarily true. The Java compiler could conceivably generate a class file for Lava that isn't compatible with Volcano. If the Lava class doesn't refer to Volcano, you could potentially change the name of the method Volcano invokes from the Lava class, and then recompile only the Lava class. If you tried to run your program using the new version of Lava, but still using the old version of Volcano that wasn't recompiled since you made your change to Lava, the Java virtual machine would, as a result of pass four class file verification, throw a NoSuchMethodError when Volcano attempted to invoke the now non-existent method in Lava.

In this case, the change to class Lava broke binary compatibility with the pre-existing class file for Volcano. In practice, this situation may arise when you update a library you have been using, and your existing code isn't compatible with the new version of the library. To make it easier to alter the code for libraries, the Java programming language was designed to allow you to make many kinds of changes to a class that don't require recompilation of classes that depend upon it. The changes you are allowed to make, which are listed in the Java Language Specification, are called the rules of binary compatibility. These rules clearly define what can be changed, added, or deleted in a class without breaking binary compatibility with pre-existing class files that depend on the changed class. For example, it is always a binary compatible change to add a new method to a class, but never to delete a method that other classes are using. So in the case of Lava, you violated the rules of binary compatibility when you changed the name of the method used by Volcano, because you in effect deleted the old method and added a new. If you had, instead, added the new method and then rewritten the old method so it calls the new, that change would have been binary compatible with any pre-existing class file that already used Lava, including Volcano.

Safety Features Built Into the Java Virtual Machine

Once the Java virtual machine has loaded a class and performed passes one through three of class file verification, the bytecodes are ready to be executed. Besides the verification of symbolic references (pass four of class file verification), the Java virtual machine has several other built-in security mechanisms operating as bytecodes are executed. These mechanisms, most of which are elements of Java's type safety, are listed in Chapter 1 as features of the Java programming language that make Java programs robust. They are, not surprisingly, also features of the Java virtual machine:

By granting a Java program only type-safe, structured ways to access memory, the Java virtual machine makes Java programs more robust, but it also makes their execution more secure. A program that corrupts memory, crashes, and possibly causes other programs to crash represents one kind of security breach. If you are running a mission critical server process, for example, it is critical that the process doesn't crash. This level of robustness is also important in embedded systems, such as a cell phone, which people don't usually expect to have to reboot. Another reason unrestrained memory access would be a security risk is because a wily cracker could potentially use it to subvert the security system. If, for example, a cracker could learn where in memory a class loader is stored, it could assign a pointer to that memory and manipulate the class loader's data. By enforcing structured access to memory, the Java virtual machine yields programs that are robust, but also frustrates crackers who dream of harnessing the internal memory of the Java virtual machine for their own devious plots.

Another safety feature built into the Java virtual machine--one that serves as a backup to structured memory access--is the unspecified manner in which the runtime data areas are laid out inside the Java virtual machine. The runtime data areas are the memory areas in which the Java virtual machine stores the data it needs to execute a Java application: Java stacks (one for each thread), a method area, where bytecodes are stored, and a garbage-collected heap, where the objects created by the running program are stored. If you peer into a class file, you won't find any memory addresses. When the Java virtual machine loads a class file, it decides where in its internal memory to put the bytecodes and other data it parses from the class file. When the Java virtual machine starts a thread, it decides where to put the Java stack it creates for the thread. When it creates a new object, it decides where in memory to put the object. Thus, a cracker cannot predict by looking at a class file where in memory the data representing that class, or objects instantiated from that class, will be kept. What's worse (for the cracker) is the cracker can't tell anything about memory layout by reading the Java virtual machine specification either. The manner in which a Java virtual machine lays out its internal data is not part of the specification. The designers of each Java virtual machine implementation decide which data structures their implementation will use to represent the runtime data areas, and where in memory their implementation will place them. As a result, even if a cracker were somehow able to break through the Java virtual machine's memory access restrictions, they would next be faced with the difficult task of finding something to subvert by looking around.

The prohibition on unstructured memory access is not something the Java virtual machine must actively enforce on a running program; rather, it is intrinsic to the bytecode instruction set itself. Just as there is no way to express an unstructured memory access in the Java programming language, there is also no way to express it in bytecodes--even if you write the bytecodes by hand. Thus, the prohibition on unstructured memory access is a firm barrier against the malicious manipulation of memory.

There is, however, a way to penetrate the security barriers erected by the mechanisms that support type safety in a Java virtual machine. Although the bytecode instruction set doesn't give you an unsafe, unstructured way to access memory, there is a way you can go around bytecodes: native methods. Basically, when you call a native method, Java's security sandbox becomes dust in the wind. First of all, the robustness guarantees don't hold for native methods. Although you can't corrupt memory from a Java method, you can from a native method. But most importantly, native methods don't go through the Java API (they are how you go around the Java API) so the security manager isn't checked before a native method attempts to do something that could be potentially damaging. (This is, of course, often how the Java API itself gets anything done. But the native methods used by the Java API are "trusted.") Thus, once a thread gets into a native method, no matter what security policy was established inside the Java virtual machine, it doesn't apply anymore to that thread, so long as that thread continues to execute the native method. This is why the security manager includes a method that establishes whether or not a program can load dynamic libraries, which are necessary for invoking native methods. Untrusted applets, for example, aren't allowed to load a new dynamic library, therefore they can't install their own new native methods. They can, however, call methods in the Java API, methods which may be native, but which are always trusted. When a thread invokes a native method, that thread leaps outside the sandbox. The security model for native methods is, therefore, the same security model described earlier as the traditional approach to computer security: you have to trust a native method before you call it.

One final mechanism built into the Java virtual machine that contributes to security is structured error handling with exceptions. Because of its support for exceptions, the Java virtual machine has something structured to do when a security violation occurs. Instead of crashing, the Java virtual machine can throw an exception or an error, which may result in the death of the offending thread, but shouldn't crash the system. Throwing an error (as opposed to throwing an exception) almost always results in the death of the thread in which the error was thrown. This is usually a major inconvenience to a running Java program, but won't necessarily result in termination of the entire program. If the program has other threads doing useful things, those threads may be able to carry on without their recently departed colleague. Throwing an exception, on the other hand, may result in the death of the thread, but is often just used as a way to transfer control from the point in the program where the exception condition arose to the point in the program where the exception condition is handled.

The Security Manager and the Java API

The first three prongs of Java's security model -- the class loader architecture, class file verifier, and safety features built into Java -- all work together to achieve a common goal: to protect the internal integrity of a Java virtual machine instance and the application it is running from malicious or buggy code it may load. By contrast, the fourth prong of the security model, the security manager, is geared towards protecting assets external to the virtual machine from malicious or buggy code running within the virtual machine. The security manager is a single object that serves as the central point for access control -- the controlling of access to external assets -- within a running Java virtual machine.

The security manager defines the outer boundaries of the sandbox. Because it is customizable, the security manager allows a custom security policy to be established for an application. The Java API enforces the custom security policy by asking the security manager for permission before it takes any action that is potentially unsafe. To ask the security manager for permission, the methods of the Java API invoke "check methods" on the security manager object. These methods are called check methods because their names all begin with the substring "check." For example, the security manager's checkRead() method determines whether or not a thread is allowed to read to a specified file. The checkWrite() method determines whether or not a thread is allowed to write to a specified file. The implementation of these methods is what defines the custom security policy of the application.

Because the Java API always checks with the security manager before it performs a potentially unsafe action, the Java API will not perform any action forbidden under the security policy established by the security manager. If the security manager forbids an action, the Java API won't perform that action.

When a Java application starts, it has no security manager, but the application can install one at its option by passing a reference to an instance of java.lang.SecurityManager or one of its subclasses to setSecurityManager(), a static method of class java.lang.System. If an application does not install a security manager, there are no restrictions placed on any activities requested of the Java API--the Java API will do whatever it is asked. (This is why Java applications, by default, do not have any security restrictions such as those that limit the activities of untrusted applets.) If the application does install a security manager, then in 1.0 or 1.1 that security manager will be in charge for the entire remainder of the lifetime of that application. It can't be replaced, extended, or changed. From that point on, the Java API will only fulfill those requests that are sanctioned by the security manager. In 1.2, however, the currently installed security manager can be replaced by code that has permission to replace it by invoking System.setSecurityManager() with a reference to a different security manager object.

In general, a "check" method of the security manager throws a security exception if the checked upon activity is forbidden, and simply returns if the activity is permitted. Therefore, the procedure a Java API method generally follows when it is about to perform a potentially unsafe activity involves two steps. First, the Java API code checks whether a security manager has been installed. If not, it skips step two and goes ahead with the potentially unsafe action. Otherwise, as step two, it calls the appropriate "check" method in the security manager. If the action is forbidden, the "check" method will throw a security exception, which will cause the Java API method to immediately abort. The potentially unsafe action will never be taken. If, on the other hand, the action is permitted, the "check" method will simply return. In this case, the Java API method carries on and performs the potentially unsafe action.

As mentioned earlier in this chapter, the security manager is responsible for two things: for specifying a security policy and for enforcing that policy. The security policy, which states what kind of code will be allowed to take what kind of actions, is defined by the code of the security manager's check methods. The policy is enforced by the behavior of the check methods when they are invoked.

Prior to 1.2, java.lang.SecurityManager was an abstract class. To establish a custom security policy in 1.0 or 1.1, you had to write your own security manager by subclassing SecurityManager and implementing its check methods. Your application would instantiate and install the security manager, which from that point forward for the remainder of the life of the application would enforce the security policy you defined in the code of its check methods.

Although the customizability of the security manager was one of the greatest strengths of Java's security model, it was also a potential point of weakness. Writing a security manager is a complicated and error prone task. Any mistakes made when implementing the check methods of a security manager could potentially translate into security holes at runtime. To help make it easier and less error prone for developers and end-users to establish fine-grained security policies based on signed code, the java.lang.SecurityManager class in the version 1.2 is a concrete class that provides a default implementation of the security manager. (In the remainder of this book, this default implementation of the security manager provided with version 1.2 will be called the "concrete SecurityManager.") Your application can instantiate and install this security manager explicitly, or allow it to be installed automatically. In Sun's Java 2 SDK version 1.2, for example, you can specify that the concrete SecurityManager be installed by using the -Djava.security.manager option on the command line.

The concrete SecurityManager class allows you to define your custom policy not in Java code, but in an ASCII file called a policy file. In the policy file, you grant permissions to code sources. Permissions are defined in terms of classes that are subclasses of java.security.Permission. For example, java.io.FilePermission represents permission to read, write, execute, or delete a file. Code sources are composed of a codebase URL from which the code was loaded and a set of signers that vouched for the code. When the security manager is created, it parses the policy file and creates CodeSource and Permission objects. These objects are encapsulated in a single Policy object that expresses the policy at runtime. Only one Policy object can be installed at any one time.

Class loaders place types into protection domains, which encapsulate all the permissions granted to the code source represented by the loaded type. Each type loaded into a 1.2 virtual machine belongs to one and only one protection domain. The protection domain is remembered and is used when deciding whether or not the code will be allowed to take potentially unsafe actions.

When the check methods of the concrete SecurityManager are invoked, most of them pass the request on to a class called the AccessController. The AccessController, using the information contained in the protection domain objects of the classes whose methods are on the call stack, performs stack inspection to determine whether the action should be allowed.

The security manager has undergone quite a bit of change in 1.2. In versions 1.0 and 1.1, each check method indicates what is being checked in its method name. To check whether or not it is OK to read a certain file, the Java API invokes the checkRead() method on the security manager and passes and the path name of the file to read as a parameter. For example, before attempting to read a file named /tmp/finances.dat, the security manager invokes checkRead("/tmp/finances.dat") on the security manager. The security manager declares 28 of these check methods, which in the remainder of this chapter will be referred to as "legacy check methods." Although new methods were added to the security manager in 1.2 that would otherwise render these legacy check methods obsolete, to maintain backwards compatibility the Java API continues to call the legacy check methods just as it did in prior releases.

The 28 legacy check methods are listed here along with the potentially unsafe action that trigger's their invocation by the code of the Java API:

In 1.2, a set of permission classes was defined whose instances represent actions code is allowed to take. A new pair of check methods were added in 1.2 to class java.lang.SecurityManager, both named checkPermission():

The checkPermission() methods accept a reference to a Permission object, which indicates the action that is being requested. Thus, this method provides an alternative way to ask the security manager if it is OK to perform a potentially unsafe action. For example, to determine whether it is OK to read file /tmp/finances.dat, the Java API in 1.2 could take either of two approaches. The Java API could take the old fashioned approach, and invoke the legacy method checkRead() passing the String "/tmp/finances.dat" as a parameter. Or, the Java API could take the fresh new approach. It could create a java.io.FilePermission object, passing Strings "/tmp/finances.dat" and "read" to the FilePermission constructor. The Java API could then pass this Permission object to the security manager's checkPermission() method.

Both the old fashioned approach of invoking a legacy check method and the fresh new approach of creating a permission object and invoking checkPermission() should yield the same result. To maintain backwards compatibility with security managers that were written for 1.0 or 1.1, however, the 1.2 Java API continues to take the old fashioned approach. The 1.2 Java API continues to call the 28 legacy check methods. Nevertheless, in the concrete SecurityManager class, the legacy methods are for the most part implemented in terms of the new checkPermission() method. So by invoking the legacy method on the concrete SecurityManager, the Java API is indirectly invoking the checkPermission() method anyway. For example, the checkRead() method implementation in the concrete SecurityManager simply instantiates a new FilePermission object, passing the pathname String passed to it to the FilePermission's constructor, along with the String "read". The checkRead() method then invokes checkPermission(), passing a reference to the FilePermission object.

The Java API may at times also invoke checkPermission() directly. For new concepts of potentially unsafe actions introduced in 1.2 and later versions, no legacy check methods exist. Thus, in some situations, the Java API may create a new Permission object for which no relevant check methods exists, and pass that Permission object directly to the security manager's checkPermission() method.

In the concrete SecurityManager class, the checkPermission() method also delegates the job of deciding whether or not to allow the action to another method. The concrete SecurityManager's checkPermission() method simply invokes the static checkPermission() method of class java.security.AccessController, passing along the permission object. The AccessController class, therefore, is the actual entity responsible for enforcing the security policy when you use the concrete SecurityManager.

All of these changes in 1.2 are backwards compatible with 1.1 and 1.0. If you created a security manager for 1.1, it should still work as expected in 1.2. You can still create a custom security manager in 1.2 as well, which allows anyone with special security needs that aren't adequately addressed by the concrete SecurityManager implementation to create a different kind of security infrastructure. Most people's security needs, however, will be likely met by taking advantage of the flexibility and extensibility built into the concrete SecurityManager.

Code Signing and Authentication

A critical piece of Java's security model is the support for authentication introduced in Java 1.1 in the java.security package and its subpackages. The authentication capabilities expand your ability to establish multiple security policies by enabling you to implement a sandbox that varies depending upon who vouched for the code. Authentication allows you to verify that a set of class files was blessed as trustworthy by some party, and that the class files were not altered en route to your virtual machine. Thus, to the extent you trust the party who vouched for the code, you can ease the restrictions placed on the code by the sandbox. You can establish different security restrictions for code that is signed by different parties.

To vouch for, or sign, a piece of code, you must first generate a public/private key pair. You should keep the private key private, but can make the public key public. At the very least, you must somehow get the public key to anyone who wants to establish a security policy based on your signature. (As will be illustrated later in this section, distributing public keys is not necessarily as easy as it may seem.) Once you have a public/private key pair, you must place the class files and any other files you want to sign into a JAR file. You then use a tool, such as jarsigner from the 1.2 SDK, to sign the entire JAR file. The signer tool will first perform a one-way hash calculation on the contents of the JAR file to generate a hash. The tool will then sign the hash with your private key, and add the signed hash to the JAR file. The signed hash represents your digital signature of the contents of the JAR file. When you distribute the JAR file that contains the signed hash, anyone with your public key can verify two things about the JAR file: the JAR file was indeed signed by you, and the contents of the JAR file were not in any way altered since you attached your signature.

The first step in the digital signing process is the one-way hash calculation, which takes a big number as input and generates a small number, called the hash. In the case of a JAR file, the big-number input to the calculation is the stream of bytes that make up the contents of the JAR file. The one-way hash calculation is called "one-way" because given just the hash (the small number), it is impossible to calculate the input (the big number). In other words, the hash value doesn't contain enough information about the input to enable the input to be regenerated from the hash. The calculation goes just one way, from big to small, from input to hash.

The hash, which is also called a message digest, serves as a kind of "fingerprint" for the input. Although different inputs can produce the same hash, the hash is considered unique enough in practice to represent the input from which it was generated. Much like a fingerprint can be used to identify the individual who made the fingerprint, a hash can be used to identify the input that caused the one-way hash algorithm to produce the hash. The hash is used during the authentication process to verify that the input is identical to the input that produced the original hash, in other words, that the input was not changed en route to its destination.

Given that it is impossible to reconstruct the input given just the hash, a hash is only useful if the input is also available. Thus, you normally transmit both input and hash together. By themselves, the combination of an input and its hash is not secure, however, because even an extremely unimaginative cracker could simply replace both the input and the hash. To prevent this scenario, you encrypt the hash with your private key before sending it. The reason you encrypt the hash rather than simply encrypting the entire JAR file is that private key encryption is a time-consuming process. It is in general much faster to calculate a one-way hash from the JAR file contents and encrypt the hash with a private key than it is to encrypt the entire JAR file with the private key. A cracker will only be able to replace both an input and encrypted hash if the cracker has your private key, which you are supposed to keep secret. Thus, the combination of input and encrypted hash is more frustrating to a potential cracker than the mere combination of input and hash because, in theory, the cracker doesn't have your private key.

Anything encrypted with your private key can be decrypted with your public key. Public/private key pairs have the characteristic that it is prohibitively difficult given just the public key to generate the private key. If you can keep your private key out the hands of crackers, therefore, their best option is to try and replace the input with a different input that yields the same hash value. If the cracker wishes to replace one class file in your JAR file with a different class file that performs some devious act, for example, the odds are extremely high that the revised JAR file (the one that contains the devious class file) will produce a different hash. But the cracker could add random data to the JAR file until the one-way hash calculation on the altered JAR file produces the same hash value as the original. If the cracker can produce such an alternative input -- one that both helps the cracker achieve his or her nefarious goals and generates the same hash as your original input -- the cracker would not need your private key. Because the cracker's input generates the same hash value as your original input, and you have already signed that hash value with your private key, the cracker can simply place your signed hash in a JAR file with his or her input. What's to prevent a cracker from taking this approach? Unfortunately for the cracker, such an approach would likely take too much time to be feasible.

Because one-way hash algorithms generate a small number (the message digest or hash) from a big number (the input), different inputs can produce the same hash. One-way hash algorithms tend to spread out the inputs that produce the same hash sufficiently randomly that the likelihood of getting the same hash value depends primarily on the size of the hash. For example, if you use a hash value that is 8 bits wide, your one-way hash algorithm will have only 256 unique hash values from which to choose. If you have a JAR file that produces the hash value 100 and you start calculating the 8 bit hash with this algorithm on other JAR files, you shouldn't be surprised if every 256 times or so you get the hash value 100. The more bits contained in the hash, of course, the less often the algorithm will produce the same hash. In practice, 64- and 128-bit hash values are common, which are considered large enough to render the process of finding a different input that produces the same hash computationally infeasible. The main barrier preventing a cracker from replacing your benevolent input with a malicious input that serves the cracker's evil purposes and produces the same hash, therefore, is the time and resources the cracker would have to devote to searching for that malicious input.

The last step in the digital signing process, after you have generated the hash value and encrypted it with your private key, is to add the encrypted hash value to the same JAR file that contains the files from which you generated the hash value originally. A signed JAR file, therefore, contains the input -- the class and data files you wanted to vouch for -- plus the hash value (generated from the input) encrypted with your private key. The encrypted hash represents your digital signature of the class and data files contained in the same JAR file. The process of signing a JAR file is shown graphically in Figure 3-3.



Figure 3-3. Digitally signing a JAR file.

To authenticate a JAR file that you have purportedly signed, the recipient must decrypt the signed hash with your public key. The result should be equal to the original hash that you calculated on the contents of the JAR file. To verify that the JAR file contents were not changed since you signed them, the recipient simply applies the one-way hash algorithm on the contents of the JAR file, just as you did during the signing process. (Remember you never encrypted contents of the JAR file, so anyone can see them. You only added a digital signature to the JAR file.) If the hash value generated by the algorithm matches the decrypted hash value, the recipient concludes that you did indeed vouch for this JAR file and that the contents of the JAR file did not change since you added your signature. The code contained in the JAR file can be placed inside a relaxed sandbox that represents the trust the recipient places in your signature. The process of verifying a digitally signed JAR file is shown in Figure 3-4.



Figure 3-4. Authenticating a digitally signed JAR file.

Although the authentication technology first introduced in Java version 1.1 is firmly founded in reliable mathematics, the math doesn't solve every problem. In fact, several questions are raised by Java's authentication technology. For example, the authentication technology says nothing about who you should trust, and to what extent you should trust them. To what extent do you trust some small company that you've never heard of? To what extent do you trust a big company whose name is a household word? To what extent do you trust a different department in your own company? What are the chances that any particular company (or department) has a rogue employee who managed to slip a time bomb into a JAR file the company signed? No cryptographic algorithm can answer these questions for you.

Another security issue stems from the assumption inherent in the authentication technology that private keys will be kept under lock and, well, key. If private keys are not kept private, the entire authentication scheme is reduced to strenuous mathematical activity that is not only ineffective, but dangerous, because it can give a false sense of security. You are responsible for keeping your own private keys private. You can only hope that any entity on whose signature authority you grant code access to your system has kept their private keys private. For any party, establishing a key management scheme that prevents private keys from being leaked (remember those rogue employees?) can be a challenging task.

Another question raised by the technology involves the distribution of public keys. Although it may seem surprising at first, the assumption inherent in the authentication technology that public keys will be made public creates some security issues of its own. For example, imagine you want to relax your sandbox for code vouched for by a guy named Evan. To do so, you need Evan's public key. But how exactly do you get his public key? If you know Evan personally, you can invite him over for coffee and ask him to bring his public key so he can give it to you in person. But what if you don't know Evan personally? You might think you could simply visit Evan's web site and grab his public key off a web page. Or alternatively, perhaps you could phone Evan and ask him to send you his public key in an e-mail. Evan should have no problem sending you, a stranger, his public key, because public keys after all are designed to be public. Evan doesn't need to worry about who gets his public key. He could hire a biplane to write his public key on the sky over Silicon Valley and still feel confident he was operating within the rules delineated by Java's authentication technology. So what's the problem? The problem is that even though Evan doesn't need to worry about your identity when he sends you his public key, you need to worry about his. Evan will be happy to send you his public key, but how do you know that the public key you receive is really the one that Evan sent?

The difficulty of public key distribution is that no matter what the means of communication, the message -- the public key -- could potentially be tampered with in transit. When you visit Evan's web page, it is possible that the web page is intercepted and changed en route to your browser, perhaps by Dastardly Doug, a cracker of international repute. When you think you are copying Evan's public key off his web page, you could actually be copying Dastardly Doug's. Doug could also have intercepted Evan's e-mail and replaced Evan's beneficent public key with his own dastardly public key. Doug could even have donned one of his many clever disguises and piloted the biplane high above Silicon Valley, inscribing his public key among the clouds in place of Evan's. If Doug can successfully replace Evan's public key with his own, Doug can pretend to be Evan and take advantage of the trust you place in Evan's signature to break into your system.

But wait a minute, isn't the difficulty of public key distribution just another authentication problem, the kind of problem the authentication technology itself is designed to address? In fact it is, and by turning authentication back on itself, Evan can make it far more difficult for Dastardly Doug to replace Evan's public key with his own.

To address the difficulties of public key distribution, several certificate authorities have been established for the purpose of vouching for public keys. Evan, for example, could go to a certificate authority and present his credentials (birth certificate, drivers license, passport, and so on) and his public key. Once convinced that Evan is who he says he is, the certificate authority would sign Evan's public key with the certificate authority's private key. The resulting sequence of numbers is called a certificate. Instead of distributing his public key, then, Evan would distribute his certificate.

You could grab Evan's certificate off of his web page, out of an e-mail, or via any other unsecured communications medium. When you get the certificate, you decrypt it with the certificate authority's public key and are rewarded with Evan's public key. The certificate scheme makes it much less likely that Doug will be able to swap his public key for Evan's because to do so, Doug would need the certificate authority's private key.

Although certificates improve the public key distribution situation immensely, some issues still remain. First of all, how exactly to you get the certificate authority's public key? You need this public key to authenticate the public keys of anyone else. Well, if you know any employees of the certificate authority personally, you could invite them over for coffee and ask them to bring their public key to give to you in person. But what if you don't know any employees of the certificate authority personally? And then there is the nagging question: why should you trust the certificate authority? A certificate authority can pretend to be anyone. Isn't a certificate authority just as susceptible as the next company to the vagaries of rogue employees?

Despite all these issues, the code signing capabilities introduced in Java 1.1 generally offer you enough security to enable you to relax your sandbox when you need to. Although the authentication technology doesn't eliminate all risk associated with relaxing the sandbox, it can help minimize the risks. Security is a tradeoff between cost and risk: the lower the security risk, the higher the cost of security. You must weigh the costs associated with any computer or network security strategy against the costs of the theft or destruction of the information or computing resources being protected. The nature of your computer or network security strategy should be shaped by the value of the assets you are trying to protect. Java's authentication technology is a useful tool that, in concert with Java's sandbox, can help you manage the costs and risks of running network-mobile code on your systems.

A Code Signing Example

For an example of code signing with the jarsigner tool of the Java 2 SDK 1.2, consider the following types, Doer, Friend, and Stranger. The first type, Doer, defines an interface that the other two types, classes Friend and Stranger implement:

// On CD-ROM in file
// security/ex2/com/artima/security/doer/Doer.java
package com.artima.security.doer;

public interface Doer {

    void doYourThing();
}

Doer declares just one method, doYourThing(). Class Friend and class Stranger implement this method in the exact same way. In fact, besides their names, the two classes are identical:

// On CD-ROM in file
// security/ex2/com/artima/security/friend/Friend.java
package com.artima.security.friend;
import com.artima.security.doer.Doer;
import java.security.AccessController;
import java.security.PrivilegedAction;

public class Friend implements Doer {

    private Doer next;
    private boolean direct;

    public Friend(Doer next, boolean direct) {
        this.next = next;
        this.direct = direct;
    }

    public void doYourThing() {

        if (direct) {

            next.doYourThing();
        }
        else {
            AccessController.doPrivileged(
                new PrivilegedAction() {
                    public Object run() {
                        next.doYourThing();
                        return null;
                    }
                }
            );
        }
    }
}

// On CD-ROM in file
// security/ex2/com/artima/security/stranger/Stranger.java
package com.artima.security.stranger;
import com.artima.security.doer.Doer;
import java.security.AccessController;
import java.security.PrivilegedAction;

public class Stranger implements Doer {

    private Doer next;
    private boolean direct;

    public Stranger(Doer next, boolean direct) {
        this.next = next;
        this.direct = direct;
    }

    public void doYourThing() {

        if (direct) {

            next.doYourThing();
        }
        else {
            AccessController.doPrivileged(
                new PrivilegedAction() {
                    public Object run() {
                        next.doYourThing();
                        return null;
                    }
                }
            );
        }
    }
}

These types -- Doer, Friend, and Stranger -- are designed to illustrate the stack inspection mechanism of the access controller. The motivation behind their design will be made clear later in this chapter, when several examples of stack inspection are given. At this point, however, the class files generated by compiling Friend and Stranger must be signed to prepare them for the upcoming stack inspection examples. The class files generated from Friend.java will be signed by a party referred to fondly as "friend." The class files generated from Stranger.java will be signed by a party referred to somewhat suspiciously as "stranger." The class file generated by Doer will not be signed.

To prepare the class files for signing, they must first be placed into JAR files. Because the class files for Friend and Stranger need to be signed by two different parties, they will be collected into two different JAR files. The two class files generated by compiling Friend.java, Friend.class and Friend$1.class, will be placed into a JAR file called friend.jar. Similarly, the two class files generated by compiling Stranger.java, Stranger.class and Stranger$1.class, will be placed into a JAR file called stranger.jar. (Note that although all of the files in these examples are in the security/ex2 directory of the CD-ROM, to repeat any of the commands that generate files, you'll have to copy the entire security/ex2 directory hierarchy to a writable media, such as a hard disk. But you probably knew that already.)

Friend.java's class files are dropped by the javac compiler in the security/ex2/com/artima/security/friend directory. Because class Friend is declared in the com.artima.security.friend package, Friend.java's class files must be placed in the JAR file in the com/artima/security/friend directory. The following command, executed in the security/ex2 directory, will place Friend.class and Friend$1.class into a newly created JAR file called friend.jar, which is placed in the current directory, security/ex2:

jar cvf friend.jar com/artima/security/friend/*.class

Once the previous command completes, the class files for Friend.java must be removed so they won't be found by the Java virtual machine when it runs the access control examples:

rm com/artima/security/friend/Friend.class
rm com/artima/security/friend/Friend$1.class

Filling a JAR file with Stranger.java's class files, which are dropped by javac in the security/ex2/com/artima/security/stranger directory, requires a similar process. From the security/ex2 directory, the following command must be executed:

jar cvf stranger.jar com/artima/security/stranger/*.class
rm com/artima/security/stranger/Stranger.class
rm com/artima/security/stranger/Stranger$1.class

To sign a JAR file with the jarsigner tool from the Java 2 SDK 1.2, a public/private key pair for the signer must already exist in a keystore file, which is a file for storing named, password-protected keys. The keytool program of the Java 2 SDK 1.2, can be used to generate a new key pair, associate the key pair with a name or alias, and protect it with a password. The alias, which is unique within each keystore file, is used to identify a particular key pair in a particular keystore file. The password for a key pair is required to access or change the information contained in the keystore file for that key pair.

The access control examples expect a keystore file named ivjmkeys in the security/ex2 directory containing key pairs for the aliases "friend" and "stranger." The following command, executed from the security/ex2 directory, will generate the key pair for the alias, friend, with the password, friend4life. In the process, it will create the keystore file named ijvmkeys:

keytool -genkey -alias friend -keypass friend4life -validity 10000 -
keystore ijvmkeys

The -validity 10000 command line argument of the previous keytool command indicates that the key pair should be valid for 10000 days, which at over 27 years, is likely enough time to outlive the product lifecycle of this edition of this book. When the command runs, it will prompt for a keystore password, which is a general password required for any kind of access or change of the keystore file. The keystore password given to ijvmkeys is "ijvm2ed".

The key pair for stranger can be generated with a similar command:

keytool -genkey -alias stranger -keypass stranger4life -validity 10000 -
keystore ijvmkeys

Now that the keystore file ijvmkeys contains key pairs for friend and stranger, and the JAR files friend.jar and stranger.jar contain the appropriate class files, the JAR files can finally be signed. The following jarsigner command, executed from the examples/ex2 directory, will sign the class files contained in friend.jar using friend's private key:

jarsigner -keystore ijvmkeys -storepass ijvm2ed -keypass friend4life
friend.jar friend

A similar command will sign the class files contained in stranger.jar with stranger's private key:

jarsigner -keystore ijvmkeys -storepass ijvm2ed -keypass stranger4life
stranger.jar stranger

Whew, that was a lot of work just to sign two JAR files. And keep in mind that in the real world, you'd have to make sure no one with bad intent got a hold of your private keys, and that you kept track of them. That means not losing the keystore file, remembering the passwords, and so on. In addition, you'll have to get your public keys to anyone who is going to use your signature to give your code access to their system.

Policy

As mentioned previously, one of the greatest advantages of Java's sandbox security model is that the sandbox can be customized. The code signing and authentication technology introduced in Java version 1.1 enables your running application to differentiate code to which you attribute different degrees of trust. By customizing the sandbox, trusted code can be given more access to system resources than untrusted code. This prevents untrusted code from accessing the system, but allows trusted code to access the system and do useful work. The real power of Java's security architecture, however, lies in the ability to grant code with varying degrees of trust different levels of partial access to the system.

Microsoft offers an authentication technology similar to Java's for ActiveX controls, but ActiveX controls don't run inside a sandbox. Thus with ActiveX, a chunk of mobile code is either completely trusted or completely untrusted. If untrusted, the ActiveX control is denied the opportunity to run. If trusted, the ActiveX control is allowed to run and given full access to the system. While this is a big improvement over no authentication at all, if some malicious or buggy code gets authenticated, the dangerous code has full access to the system. One of the strengths of Java's security architecture is that code can be given access only to the resources it needs. If some malicious or buggy code gets authenticated, it has less opportunity to do damage. For example, instead of being able to delete all files on a local hard disk, the malicious or buggy code might only be able to delete the files in a particular directory set aside just for it.

One major goal of the 1.2 security infrastructure is to make it easier and less error prone to establish fine-grained access control policies based on signed code. To be able to assign different system access privileges to different units of code, Java's access control mechanism must be able to ascertain what privileges should be given to each individual piece of code. To facilitate this process, each piece of code (each class file) loaded into a 1.2 or beyond Java virtual machine is associated with a code source. The code source basically says where the code came from and who, if anyone, has vouched for the code by signing it. In the 1.2 security model, permissions (system access privileges) are assigned to code sources. Thus, if a piece of code requests access to a particular system resource, the Java virtual machine will grant the code access to that resource only if such access is a privilege associated with that code's code source.

In the 1.2 security infrastructure, an access control policy for an entire Java application is represented by a single instance of a subclass of the abstract class java.security.Policy. Each application has just one Policy object in effect at any given time. Code that has permission can replace the current Policy object with a new one by invoking Policy.setPolicy() and passing a reference to the new Policy object. Class loaders consult the Policy object to help them to decide what privileges to grant code as they import the code into the virtual machine.

A security policy is a mapping from a set of properties that characterize running code to the permissions granted the code. In the 1.2 security infrastructure, the properties that characterize running code are collectively called the code source. A code source is represented by a java.security.CodeSource object, which contains a java.net.URL to represent the codebase and an array of zero or more certificate objects to represent the signers. Certificate objects are instances of subclasses of the abstract class java.security.cert.Certificate. A Certificate is an abstraction that represents a binding of a principal to a public key, and another principal (the certificate authority mentioned previously) that vouches for that binding. The CodeSource object contains an array of Certificate objects, because the same code can be signed (vouched for) by more than one party. The signatures are usually obtained from a JAR file.

All of the tools and access control infrastructure that accompanies the concrete SecurityManager in version 1.2 work only with certificates. None work with bare public keys. If you don't have a certificate authority handy, you can sign your own public key with your private key and generate a self-signed certificate. The keytool program from the Java 2 SDK version 1.2 always generates a self-signed certificate when it generates keys. In the code signing example given earlier in this chapter, for instance, the keytool created not only public/private key pairs, but also self-signed certificates for the aliases friend and stranger.

A permission is represented by an instance of a subclass of the abstract class java.security.Permission. A permission object has three properties: a type, a name, and an optional action. A permission's type is indicated by the name of the permission class. Some examples of permission types are: java.io.FilePermission, java.net.SocketPermission, and java.awt.AWTPermission. A permission's name is encapsulated inside the Permission object. For example, the name of a FilePermission might be: "/my/finances.dat"; the name of an SocketPermission might be "applets.artima.com:2000"; and the name of an AWTPermission might be "showWindowWithoutBannerWarning". The third property of a Permission object is its action. Not all permissions have an action. An example of an action for a FilePermission is: "read,write", and for a SocketPermission is: "accept,connect". A FilePermission with the name /my/finances.dat and action read,write represents permission to read and write to the file /my/finances.dat. Both name and action are represented by Strings.

The Java API has a large hierarchy of permissions that represent potentially dangerous actions code may wish to take. You can also create your own permission classes to represent custom permissions that you use for your own purposes. For example, you could create permission classes that represent permission to access particular records of your proprietary database. Defining custom permission classes is one way you can extend the 1.2 security mechanism to reflect your own needs. If you create your own Permission classes, you can use them like any of the built-in Permission classes from the Java API.

In the Policy object, each CodeSource is associated with one or more Permission objects. The Permission objects with which a CodeSource is associated are encapsulated in an instance of a subclass of java.security.PermissionCollection. Class loaders can invoke Policy.getPolicy() to get a reference to the policy object currently in effect. They can then invoke getPermissions() on the Policy object, passing in a CodeSource to get a PermissionCollection of Permission objects for the passed CodeSource. A class loader can then use the PermissionCollection retrieved from the Policy object to help it decide what permissions the code it is about to import will be granted.

Policy File

java.security.Policy is an abstract class. One of the implementation details of concrete Policy subclasses is how an instance of the subclass learns what the policy should be. Subclasses can take various approaches, such as deserializing a previously serialized policy object, extracting the policy from a database, or reading the policy from a file. The concrete policy subclass supplied by Sun with the version 1.2 Java Platform takes the latter approach: it enables you to express your security policy in a context free grammar in an ASCII policy file.

A policy file is consists of a series grant clauses, each of which grants a code source a set of permissions. As mentioned previously, a code source consists of a codebase, which is a URL from which code was loaded, and a set of signers. In the policy file, signers are designated with the alias with which the signer's public key is stored in a keystore file. The keystore can be explicitly specified in the policy file in a keystore statement.

As an example of a policy file, consider the policyfile.txt file from the security/ex2 directory of the CD-ROM:

keystore "ijvmkeys";

grant signedBy "friend" {
    permission java.io.FilePermission "question.txt", "read";
    permission java.io.FilePermission "answer.txt", "read";
};

grant signedBy "stranger" {
    permission java.io.FilePermission "question.txt", "read";
};

grant codeBase "file:${com.artima.ijvm.cdrom.home}/security/ex2/*" {
	permission java.io.FilePermission "question.txt", "read";
	permission java.io.FilePermission "answer.txt", "read";
};

The first statement in the policyfile.txt file is a keystore statement:

keystore "ijvmkeys";

This keystore statement indicates that the key aliases mentioned in the rest of the policy file refer to certificates stored in a file named "ijvmkeys". Because this filename includes no path, the file must be located in the current directory -- the directory in which the Java application using this policy file is started.

The second statement in the policy file is a grant statement:

grant signedBy "friend" {
    permission java.io.FilePermission "question.txt", "read";
    permission java.io.FilePermission "answer.txt", "read";
};

This statement grants two permissions to any code signed by the entity with the alias "friend". The granted permissions are: permission to read a file named question.txt and permission to read a file named answer.txt. Because these filenames appear with no path, both files must be in the current directory, the directory in which the application is started. Because no codebase is mentioned in the grant clause, code signed by friend can come from any codebase. All code signed by friend, regardless of codebase, will be awarded permission to read question.txt and answer.txt.

The third statement in policyfile.txt is another grant statement, similar in form to the first:

grant signedBy "stranger" {
    permission java.io.FilePermission "question.txt", "read";
};

This statement grants one permission to any code signed by the entity with the alias "stranger": permission to read a file named question.txt. This file must be sitting in the current directory, the directory in which the application is started. Because no codebase is mentioned in the grant clause, code signed by stranger can come from any codebase and will still be awarded permission to read question.txt. Note that although stranger is allowed to read the question contained in question.txt, stranger is not allowed to peek at the answer contained in answer.txt. This contrasts with the privileges awarded to friend, who is allowed to read both the question and the answer.

The fourth and final statement in this policy file is yet another grant statement:

grant codeBase "file:${com.artima.ijvm.cdrom.home}/security/ex2/*" {
	permission java.io.FilePermission "question.txt", "read";
	permission java.io.FilePermission "answer.txt", "read";
};

This final grant statement grants two permissions to any code that was loaded from a particular directory, permission to read a file named question.txt and permission to read a file named answer.txt. Both files must be in the current directory, the directory in which the application is started. Note that this grant statement does not mention any signers. The code can be signed by anyone or no one. So long as it is loaded from the indicated directory, the code will be granted the listed permissions.

The codebase URL in this grant statement takes the form of a file: URL that includes a property, ${com.artima.ijvm.cdrom.home}. If you run the AccessControl example programs described later in this chapter, you'll have to set the com.artima.ijvm.cdrom.home property to the path of the CD-ROM that comes with this book, or to whatever directory you have moved the security subdirectory from the CD-ROM. The Policy object that is instantiated based on the contents of policyfile.txt will take the com.artima.ijvm.cdrom.home property into account when it constructs the URL for the CodeSource for this grant clause.

Protection Domains

As class loaders load types into the Java virtual machine, they assign each type into a protection domain. A protection domain defines all the permissions that are granted to a particular code source. (A protection domain corresponds to one or more grant clauses in a policy file.) Each type loaded into a Java virtual machine belongs to one and only one protection domain.

The class loader knows the codebase and the signers of any class or interface it loads. It uses that information to create a CodeSource object. It passes the CodeSource object to the getPermissions() method of the currently in-force Policy object to get an instance of a subclass of the abstract class java.security.PermissionCollection. The PermissionCollection holds references to all Permission objects granted to the given code source by the current policy. With both the CodeSource that it created and the PermissionCollection it got from the Policy object, it can instantiate a new ProtectionDomain object. It places the code into a protection domain by passing the appropriate ProtectionDomain object to the defineClass() method, an instance method of class ClassLoader that user-defined class loaders call to import type data into the Java virtual machine. This assigning classes into protection domains is a critical job which, as mentioned earlier in this chapter, is one of three ways the class loader architecture supports Java's sandbox security model.

Although the Policy object represents a global mapping from code sources to permissions, in the end the class loader is the responsible party that decides what permissions code is going to get when it runs. A class loader could, for example, completely ignore the current policy and just assign permissions off the cuff. Or, a class loader could add permissions to those returned by the policy object's getPermissions() method. For example, a class loader for loading applet code could add a permission to make a socket connection back to the host from which the applet came to the permissions, if any, granted to the code by the policy. As you can see, the class loader plays a crucial security role as it loads classes.

For a graphical depiction of protection domains, code sources, and permissions, consider Figure 3-5. In Figure 3-5, the method area and heap are shown after the code inside friend.jar is loaded under the policy defined by policyfile.txt. friend.jar is a JAR file in the security/ex2/jars directory of the CD-ROM, and policyfile.txt is an ASCII policy file in the security/ex2 directory. The friend.jar file contains two class files, Friend.class and Friend$1.class. As described in the code signing example earlier in this chapter, both of these class files have been signed by friend. When these classes are defined by the class loader, they are placed into a protection domain whose CodeSource object indicates two things. First, the CodeSource indicates that the class files were loaded from a local jar file, whose URL is: file:///f|/security/ex2/jars/friend.jar. And second, the CodeSource indicates that the class files were signed by friend, an alias associated with a certificate in the local keystore. The ProtectionDomain object encapsulates a reference to the CodeSource object and to a java.security.Permissions object. java.security.Permissions, a concrete subclass of the abstract java.security.PermissionCollection class, represents a heterogeneous collection of permissions. The Permissions object holds references to two java.io.FilePermission objects. These two FilePermissions grant the privilege to read files named question.txt and answer.txt in the current directory.

When a class loader imported Friend and Friend$1 into the method area shown in Figure 3-5, the class loader passed a reference to the ProtectionDomain object to defineClass() along with the bytes of the class files. The defineClass() method associated the type data in the method area for Friend and Friend$1 with the passed ProtectionDomain object. This association is shown graphically in Figure 3-5, which includes arrows that represent references to the ProtectionDomain object held as part of the type data in the method area for Friend and Friend$1.



Figure 3-5. Protection domains, code sources, and permissions.

The Access Controller

Class java.security.AccessController provides a default security policy enforcement mechanism that uses stack inspection to determine whether potentially unsafe actions should be permitted. The access controller can't be instantiated. It isn't an object. Rather, it is a bundle of static methods collected in a single class. The central method of the AccessController is its static checkPermission() method, which decides whether or not a particular action is allowed. This method returns void and takes a reference to a Permission object as its only parameter. Similar to the check methods of the security manager, if the AccessController decides the operation should be allowed, its checkPermission() method simply returns silently. But if the AccessController decides that an operation should be forbidden, its checkPermission() method completes abruptly by throwing an AccessControlException, or one of its subclasses.

As mentioned previously, the concrete SecurityManager's implementation of the legacy check methods (such as checkRead() and checkWrite()) simply instantiate an appropriate Permission object and invoke the concrete SecurityManager's checkPermission() method. The concrete SecurityManager's checkPermission() method simply invokes checkPermission() on the AccessController. Thus, if you install the concrete SecurityManager, the AccessController is the ultimate entity that decides whether or not potentially unsafe actions will be allowed.

The basic algorithm implemented by the AccessController's checkPermission() method makes certain that every frame on the call stack has permission to perform the potentially unsafe action. Each stack frame represents some method that has been invoked by the current thread. Each method is defined in some class. Each class belongs to some protection domain. And each protection domain contains a set of permissions. Thus, each stack frame is indirectly associated with a set of permissions. For an action represented by the Permission object passed to the AccessController's checkPermission() method to be allowed, the basic algorithm of the AccessController requires that the permissions associated with each frame on the call stack must include or imply the Permission passed to checkPermission().

The AccessController's checkPermission() method inspects the stack from the top down. As soon as it encounters a frame that doesn't have permission, it throws an AccessControlException. By throwing the exception, the AccessController, indicates that the action should not be allowed. On the other hand, if the checkPermission() method reaches the bottom of the stack without encountering any frames that don't have permission to perform the potentially unsafe action, checkPermission() returns silently. By returning rather than throwing an exception, the AccessController indicates the action should be allowed

The actual algorithm implemented by the AccessController's checkPermission() method is a bit more complex than the basic algorithm described here. By invoking any of several doPrivileged() methods of class AccessController, programs can in effect cause the AccessController to stop its frame by frame search before it reaches the bottom of the stack. The doPrivileged() method will be described later in this chapter.

The implies() Method

To determine whether or not the action represented by the Permission object passed to the AccessController's checkPermission() method is included among (or implied by) the permissions associated with the code on the call stack, the AccessController makes use of an important method named implies(). The implies() method is declared in class Permission, as well as in classes PermissionCollection and ProtectionDomain. implies() takes a Permission object as its only parameter and returns a boolean true or false. The implies() method of class Permission determines whether the permission represented by one Permission object is naturally implied by the permission represented by a different Permission object. The implies() methods of PermissionCollection and ProtectionDomain determine whether the passed Permission is included among or implied by the collection of Permission objects encapsulated in the PermissionCollection or ProtectionDomain.

For example, a permission to read all files in the /tmp directory would naturally imply a permission to read /tmp/f, a specific file in the /tmp directory, but not vice versa. If you asked a FilePermission object that represents the permission to read any file in the /tmp directory if it implies the permission to read file /tmp/f, the implies() method should return true. But if you ask a FilePermission object representing the permission to read /tmp/f if it implies the permission to read any file in the /tmp directory, the implies() method should return false.

The Example1 application from the security/ex1 directory of the CD-ROM demonstrates this meaning of implies():

import java.security.Permission;
import java.io.FilePermission;
import java.io.File;

// On CD-ROM in file security/ex1/Example1.java
class Example1 {

    public static void main(String[] args) {

        char sep = File.separatorChar;

        // Read permission for "/tmp/f"
        Permission file = new FilePermission(
            sep + "tmp" + sep + "f", "read");

        // Read permission for "/tmp/*", which
        // means all files in the /tmp directory
        // (but not any files in subdirectories
        // of /tmp)
        Permission star = new FilePermission(
            sep + "tmp" + sep + "*", "read");

        boolean starImpliesFile = star.implies(file);
        boolean fileImpliesStar = file.implies(star);

        // Prints "Star implies file = true"
        System.out.println("Star implies file = "
            + starImpliesFile);

        // Prints "File implies star = false"
        System.out.println("File implies star = "
            + fileImpliesStar);
    }
}

The Example1 application creates two FilePermission objects, one that represents read permission for a particular directory and another that represents read permission for a particular file in that same directory. The FilePermission object referenced from local variable star represents permission to read any file in /tmp. The FilePermission object referenced from local variable file represents permission to read file /tmp/f. When executed, this application prints:

Star implies file = true
File implies star = false

The implies() method is used by the AccessController to determine whether a thread has permission to take actions. If the checkPermission() method of the AccessController is invoked to determine whether that thread has permission to read file /tmp/f, for example, the AccessController can invoke the implies() method on the ProtectionDomain objects associated with each frame of that thread's call stack. To each implies() method, the AccessController can pass the FilePermission object representing permission to read file /tmp/f that was passed to its checkPermission() method. The implies() method of each ProtectionDomain object can invoke implies() on the PermissionCollection it encapsulates, passing along the same FilePermission. Each PermissionCollection can in turn invoke implies() on the Permission objects it contains, once again passing along a reference to the same FilePermission object. As soon as a PermissionCollection's implies() method encounters one Permission object whose implies() method returns true, the PermissionCollection's implies() method returns true. Only if none of the implies() methods of the Permission objects contained in a PermissionCollection return true does the PermissionCollection return false. The ProtectionDomain's implies() method simply returns what the PermissionCollection's implies() method returns. If the AccessController gets back a true from the implies() method of a ProtectionDomain associated with a particular stack frame, the code represented by that stack frame has permission to perform the potentially unsafe action.

Stack Inspection Examples

The next few sections give several examples to illustrate the manner in which the AccessController performs stack inspection. In the upcoming examples, code signed by both friend and stranger will be trusted to some extent, but friend code will be trusted more than stranger code. In particular, code signed by both friend and stranger will be given permission to read a file named question.txt, which contains a question. But although code signed by friend will be given permission to read a file named answer.txt, which contains the answer to the question asked in question.txt, code signed by stranger will not. These permissions granted to friend and stranger are those outlined in the policyfile.txt file from the security/ex2 directory of the CD-ROM, which was described earlier in this chapter. Each of the upcoming examples will take their policy from policyfile.txt.

The stack inspection examples all make use of classes that implement the Doer interface:

// On CD-ROM in file
// security/ex2/com/artima/security/doer/Doer.java
package com.artima.security.doer;

public interface Doer {

    void doYourThing();
}

To be a Doer, a class must provide an implementation for one method: doYourThing(). Classes that implement Doer can do whatever they feel like in their doYourThing() method. For example, here's a class that implements Doer named TextFileDisplayer whose "thing" is to display the contents of a text file:

// On CD-ROM in file security/ex2/TextFileDisplayer.java

import com.artima.security.doer.Doer;
import java.io.FileReader;
import java.io.CharArrayWriter;
import java.io.IOException;

public class TextFileDisplayer implements Doer {

    private String fileName;

    public TextFileDisplayer(String fileName) {
        this.fileName = fileName;
    }

    public void doYourThing() {

        try {
            FileReader fr = new FileReader(fileName);

            try {
                CharArrayWriter caw = new CharArrayWriter();

                int c;
                while ((c = fr.read()) != -1) {
                    caw.write(c);
                }

                System.out.println(caw.toString());
            }
            catch (IOException e) {
            }
            finally {
                try {
                    fr.close();
                }
                catch (IOException e) {
                }
            }
        }
        catch (IOException e) {
        }
    }
}

When you create a TextFileDisplayer object, you must pass a file path name to its constructor. The TextFileDisplayer constructor stores the passed path name in the filename instance variable. When you invoke doYourThing() on the TextFileDisplayer object, it will attempt to open and read the contents of the file and print them at the standard output.

Another example of a doYourThing() method comes from classes Friend and Stranger, which appeared earlier in this chapter in the code signing example and are shown again here to refresh your memory:

// On CD-ROM in file
// security/ex2/com/artima/security/friend/Friend.java
package com.artima.security.friend;
import com.artima.security.doer.Doer;
import java.security.AccessController;
import java.security.PrivilegedAction;

public class Friend implements Doer {

    private Doer next;
    private boolean direct;

    public Friend(Doer next, boolean direct) {
        this.next = next;
        this.direct = direct;
    }

    public void doYourThing() {

        if (direct) {

            next.doYourThing();
        }
        else {
            AccessController.doPrivileged(
                new PrivilegedAction() {
                    public Object run() {
                        next.doYourThing();
                        return null;
                    }
                }
            );
        }
    }
}

// On CD-ROM in file
// security/ex2/com/artima/security/stranger/Stranger.java
package com.artima.security.stranger;
import com.artima.security.doer.Doer;
import java.security.AccessController;
import java.security.PrivilegedAction;

public class Stranger implements Doer {

    private Doer next;
    private boolean direct;

    public Stranger(Doer next, boolean direct) {
        this.next = next;
        this.direct = direct;
    }

    public void doYourThing() {

        if (direct) {

            next.doYourThing();
        }
        else {
            AccessController.doPrivileged(
                new PrivilegedAction() {
                    public Object run() {
                        next.doYourThing();
                        return null;
                    }
                }
            );
        }
    }
}

Friend and Stranger have much in common. They have identical instance variables, constructors, and doYourThing() methods. They differ only in their package and simple names. When you create a new Friend or Stranger object, you must pass to the constructor a boolean value and a reference to another object whose class implements the Doer interface. The constructor stores the passed Doer reference in the instance variable, next, and the boolean value in the instance variable, direct. When doYourThing() is invoked on either a Friend or Stranger object, the method invokes doYourThing(), either directly or indirectly, on the Doer reference contained in next. If direct is true, Friend or Stranger's doYourThing() just invokes doYourThing() directly on next. Otherwise, Friend or Stranger's doYourThing() invokes doYourThing() on next indirectly, by way of a doPrivileged() call.

A Stack Inspection that Says "Yes"

As the first stack inspection example, consider the Example2a application:

// On CD-ROM in file security/ex2/Example2a.java
import com.artima.security.friend.Friend;
import com.artima.security.stranger.Stranger;

// This succeeds because everyone has permission to
// read answer.txt
class Example2a {

    public static void main(String[] args) {

        TextFileDisplayer tfd = new TextFileDisplayer("question.txt");

        Friend friend = new Friend(tfd, true);

        Stranger stranger = new Stranger(friend, true);

        stranger.doYourThing();
    }
}

The Example2a application creates three Doer objects: a TextFileDisplayer, a Stranger, and a Friend. The TextFileDisplayer constructor is passed the String, "question.txt". When its doYourThing() method is invoked, it will attempt to open a file named question.txt in the current directory for reading and print its contents to the standard output. The Friend object's constructor is passed a reference to the TextFileDisplayer object (a Doer) and the boolean value true. Because the passed boolean value is true, when Friend's doYourThing() method is invoked, it will directly invoke doYourThing() on the TextFileDisplayer object. The Stranger object's constructor is passed a reference to the Friend object (also a Doer) and the boolean value true. Because the passed boolean value is true, when Stranger's doYourThing() method is invoked, it will directly invoke doYourThing() on the Friend object. After creating these three Doer objects, and hooking them together as described, Example2a's main() method invokes doYourThing() on the Stranger object and the fun begins.

When the Example2a program invokes doYourThing() on the Stranger object referenced from the stranger variable, the Stranger object invokes doYourThing() on the Friend object, which invokes doYourThing() on the TextFileDisplayer object. TextFileDisplayer's doYourThing() method attempts to open and read a file named "question.txt" in the current directory (the directory in which the Example2a application was started) and print its contents to the standard output. When TextFileDisplayer's doYourThing() method creates a new FileReader object, the FileReader's constructor creates a new FileInputStream, whose constructor checks to see whether or not a security manager has been installed. In this case, the concrete SecurityManager has been installed, so the FileInputStream's constructor invokes checkRead() on the concrete SecurityManager. The checkRead() method instantiates a new FilePermission object representing permission to read file question.txt and passes that object to the concrete SecurityManager's checkPermission() method, which passes the object on to the checkPermission() method of the AccessController. The AccessController's checkPermission() method performs the stack inspection to determine whether this thread should be allowed to open file question.txt for reading.

Figure 3-6 shows the call stack when the AccessController's checkPermission() method is invoked. In this figure, each frame of the call stack is represented by a horizontal row that is composed of several elements. The leftmost element in each stack frame row, which is labeled "class," is the fully qualified name of the class in which the method represented by that stack frame is defined. The next element to the right, which is labeled "method," gives the name of the method. The next element, which is labeled "protection domain," indicates the protection domain with which each frame is associated. Farthest to the right is an arrow that shows the progression of the AccessController's checkPermission() method as it checks whether each stack frame has permission to perform the requested action. Just to the left of the arrow is a number for each stack frame. Like all images of the stack shown in this book, the top of the stack appears at the bottom of the picture. Thus, in Figure 3-6, the top of the stack is the frame numbered 10.



Figure 3-6. Stack inspection for Example2a: all frames have permission.

The protection domain column of the stack diagram shown in Figure 3-6 shows each frame associated with one of four protection domains, named "FRIEND," "STRANGER," "CD-ROM," and "BOOTSTRAP." Three of these protection domains correspond to grant clauses in policyfile.txt. The FRIEND protection domain corresponds to the grant clause that gives permission to any code signed by friend to read question.txt and answer.txt. The STRANGER protection domain corresponds to the grant clause that gives permission to any code signed by stranger to read question.txt. The CD-ROM protection domain corresponds to the grant clause that gives permission to any code loaded from the "${com.artima.ijvm.cdrom.home}/security/ex2/" directory to read question.txt and answer.txt. The fourth and final protection domain, named BOOTSTRAP, doesn't correspond to any grant clause in policyfile.txt. Rather, the BOOTSTRAP protection domain represents the permissions granted to any code loaded by the bootstrap class loader, which is responsible for loading the class files of the Java API. Code in the BOOTSTRAP protection domain is granted java.lang.AllPermission, which gives it permission to do anything.

To get the Example2a application to demonstrate stack inspection as intended, you must start the application with an appropriate command. When using the java program from the Java 2 SDK version 1.2, the appropriate command takes the form:

java -Djava.security.manager -Djava.security.policy=policyfile.txt
-Dcom.artima.ijvm.cdrom.home=d:\books\InsideJVM\manuscript\cdrom -cp
.;jars/friend.jar;jars/stranger.jar Example2a

This command, which is contained in the ex2a.bat file in the security/ex2 directory of the CD-ROM, is an example of the kind of command you'll need to use to get the example to work. By defining the java.security.manager property on the command line, you indicate you want the concrete SecurityManager to be automatically installed. Because the Example2a application doesn't install a security manager explicitly, if you neglect to define the java.security.manager property on the command line, no security manager will be installed and the code will be able do anything. The -cp argument sets up the class path, which causes the virtual machine to look for class files in the current directory and in the friend.jar and stranger.jar files in the jars subdirectory. The com.artima.ijvm.cdrom.home property indicates the directory in which Doer, Example2a, and TextFileDisplayer are located. This property is used by the third grant clause in policyfile.txt, which corresponds to the protection domain named "CD-ROM." As a result, types Doer, Example2a, and TextFileDisplayer will be loaded into the CD-ROM protection domain and granted permission to read to both question.txt and answer.txt. To execute Example2a on your own system, you must set the com.artima.ijvm.cdrom.home property to the security/ex2 directory of your CD-ROM, or to whatever directory you may have copied the security/ex2 directory from the CD-ROM.

When the AccessController performs its stack inspection, it starts at the top of the stack, frame ten, and heads down to frame one, which is the frame for the first method invoked by this thread, main() of class Example2a. In the case of the Example2a application, every frame on the call stack has permission to perform the action: to read the file "question.txt". This is because all four protection domains represented on the call stack -- FRIEND, STRANGER, CD-ROM, and BOOTSTRAP -- include or imply a FilePermission for reading question.txt in the current directory. When the AccessController's checkPermission() method reaches the bottom of the stack without having encountered any frames that don't have permission to read the file, it returns normally, without throwing an exception. The FileInputStream goes ahead and opens the file for reading. The Example2a application reads in the contents of question.txt and prints them to the standard output, which looks like this:

Too what extent does complexity threaten security?

A Stack Inspection that Says "No"

As an example of a stack inspection that results in a denied permission, consider the Example2b application from the security/ex2 directory of the CD-ROM:

// On CD-ROM in file security/ex2/Example2b.java
import com.artima.security.friend.Friend;
import com.artima.security.stranger.Stranger;

// This fails because the Stranger code doesn't have
// permission to read file question.txt

class Example2b {

    public static void main(String[] args) {

        TextFileDisplayer tfd = new TextFileDisplayer("answer.txt");

        Friend friend = new Friend(tfd, true);

        Stranger stranger = new Stranger(friend, true);

        stranger.doYourThing();
    }
}

The only difference between Example2b and the previous example, Example2a, is that whereas Example2a passes the file name "question.txt" to the TextFileDisplayer constructor, Example2b passes the file name "answer.txt". This small change to the application makes a big difference to the outcome of the program, however, because one of the methods on the stack doesn't have permission to access "answer.txt".

When the Example2b program invokes doYourThing() on the Stranger object referenced from the stranger variable, the Stranger object invokes doYourThing() on the Friend object, which invokes doYourThing() on the TextFileDisplayer object. TextFileDisplayer's doYourThing() method attempts to open and read a file named "answer.txt" in the current directory (the directory in which the Example2b application was started) and print its contents to the standard output. When TextFileDisplayer's doYourThing() method creates a new FileReader object, the FileReader constructor creates a new FileInputStream, whose constructor checks to see whether or not a security manager has been installed. In this case, the concrete SecurityManager has been installed, so the FileInputStream's constructor invokes checkRead() on the concrete SecurityManager. The checkRead() method instantiates a new FilePermission object representing permission to read file answer.txt and passes that object to the concrete SecurityManager's checkPermission() method, which passes the object on to the checkPermission() method of the AccessController. The AccessController's checkPermission() method performs the stack inspection to determine whether this thread should be allowed to open file answer.txt for reading.

The call stack to be inspected in Example2b, which is shown in Figure 3-7, looks identical to the call stack that was inspected in Example2a. The only difference is that this time, rather than making sure every frame on the stack has permission to read file question.txt, the AccessController will make sure every frame on the stack has permission to read answer.txt. As always, stack inspection starts at the top of the stack and proceeds on down the stack towards frame one. But this time, the inspection process never actually reaches frame one. When the AccessController reaches frame two, it discovers that the code of the Stranger class, to whom the doYourThing() method of frame two belongs, doesn't have permission to read "answer.txt". Because all frames of the stack must have permission, the stack inspection process need go no farther than frame two. The AccessController's checkPermission() method throws an AccessControl exception.



Figure 3-7. Stack inspection for Example2b: frame two doesn't have permission.

To get the Example2b application to work as intended, you must start the application with an appropriate command. When using the java program from the Java 2 SDK version 1.2, the appropriate command takes the form:

java -Djava.security.manager -Djava.security.policy=policyfile.txt -
Dcom.artima.ijvm.cdrom.home=d:\books\InsideJVM\manuscript\cdrom -cp
.;jars/friend.jar;jars/stranger.jar Example2b

This command, which is contained in the ex2b.bat file in the security/ex2 directory of the CD-ROM, is an example of the kind of command you'll need to use to get the example to work. As before, to execute Example2b on your own system, you must set the com.artima.ijvm.cdrom.home property to the security/ex2 directory of your CD-ROM, or to whatever directory you may have copied the security/ex2 directory from the CD-ROM. When you run this program, you should see this output:

Exception in thread "main" java.security.AccessControlException: access
denied (java.io.FilePermission answer.txt read)
	at java.security.AccessControlContext.checkPermission(AccessControlContext.java:195)
	at java.security.AccessController.checkPermission(AccessController.java:403)
	at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
	at java.lang.SecurityManager.checkRead(SecurityManager.java:873)
	at java.io.FileInputStream.(FileInputStream.java:65)
	at java.io.FileReader.(FileReader.java:35)
	at TextFileDisplayer.doYourThing(TextFileDisplayer.java, Compiled Code)
	at com.artima.security.friend.Friend.doYourThing(Friend.java:21)
	at com.artima.security.stranger.Stranger.doYourThing(Stranger.java:21)
	at Example2b.main(Example2b.java:18)

The doPrivileged() Method

The basic algorithm illustrated so far in this chapter, in which the AccessController inspects the stack from top to bottom, stubbornly requiring that every frame have permission to perform an action, prevents less trusted code from hiding behind more trusted code. Because the AccessController looks all the way down the call stack, it will eventually find any method that isn't trusted to perform the requested action. For example, even though the untrusted Stranger object of Example2b places the trusted code of Friend and TextFileDisplayer between it and the Java API method that attempts to open file answer.txt, the untrusted Stranger code is unable to hide behind that trusted code. As shown in Figure 3-7, although the AccessController must look through eight frames that have permission to read answer.txt before it encounters frame two, it eventually reaches frame two. And once it arrives at frame two, it discovers the doYourThing() method of class Stranger, whose associated protection domain doesn't have permission to read answer.txt. As a result of this discovery, the AccessController throws an AccessControllerException, thereby disallowing the read.

The basic AccessController algorithm prevents any code from performing or causing to be performed any action that the code is not trusted to do. Methods belonging to a less powerful protection domain, therefore, are unable to gain privileges by invoking methods belonging to more powerful protection domains. The basic algorithm also implies that methods belonging to more powerful protection domains must give up privileges when calling methods belonging to less powerful protection domains. Although the basic algorithm provides behavior that is desirable in general, the AccessController's stubborn insistence that all frames on the call stack have permission to perform the requested action can at times be a bit restrictive.

Sometimes code farther up the call stack (closer to the top of the stack) might wish to perform an action that code farther down the call stack may not be allowed to do. For example, imagine that an untrusted applet asks the Java API to render a string of text in bold Helvetica font on its applet panel. To fulfill this request, the Java API may need to open a font file on the local disk to load a bold Helvetica font with which to render the text on behalf of the applet. The class making the explicit request to open the font file, because it belongs to the Java API, likely has permission to open the file. However, the code of the untrusted applet, which is represented by a stack frame farther down the call stack, likely doesn't have permission to open the file. Given just the basic algorithm, the AccessController would prevent the opening of the font file because the code for the untrusted applet, sitting somewhere on the stack, doesn't have permission to open the file.

To enable trusted code to perform actions for which less trusted code farther down the call stack may not have permission to do, the AccessController class offers four overloaded static methods named doPrivileged(). Each of these methods accepts as a parameter an object that implements either the java.security.PrivilegedAction or java.security.PrivilegedExceptionAction interface. Both of these interfaces declare one method named run() that takes no parameters and returns void. The only difference between these two interfaces is that whereas PrivilegedExceptionAction's run() method declares Exception in its throws clause, PrivilegedAction declares no throws clause. To perform an action despite the existence of less trusted code farther down the call stack, you create an object that implements one of the PrivilegedAction interfaces, whose run() method performs the action, and pass that object to doPrivileged().

When you invoke doPrivileged(), as when you invoke any method, a new frame is pushed onto the stack. In the context of a stack inspection by the AccessController, a frame for a doPrivileged() method invocation signals an early termination point for the inspection process. If the protection domain associated with the method that invoked doPrivileged() has permission to perform the requested action, the AccessController returns immediately. It allows the action even if code farther down the stack doesn't have permission to perform the action.

If an untrusted applet asks the Java API to render a test string on its applet panel, therefore, the Java API code can open the local font file by wrapping the file open action in a doPrivileged() call. The AccessController will allow such a request even though the untrusted applet code doesn't have permission to open the file. Because the frame for the untrusted applet code is below the frame for the doPrivileged() invocation by the Java API code, the AccessController won't even consider the permissions of the untrusted applet code.

For an example of a doPrivileged() method invocation, consider again the doYourThing() method of class Friend:

// On CD-ROM in file
// security/ex2/com/artima/security/friend/Friend.java
package com.artima.security.friend;
import com.artima.security.doer.Doer;
import java.security.AccessController;
import java.security.PrivilegedAction;

public class Friend implements Doer {

    private Doer next;
    private boolean direct;

    public Friend(Doer next, boolean direct) {
        this.next = next;
        this.direct = direct;
    }

    public void doYourThing() {

        if (direct) {

            next.doYourThing();
        }
        else {
            AccessController.doPrivileged(
                new PrivilegedAction() {
                    public Object run() {
                        next.doYourThing();
                        return null;
                    }
                }
            );
        }
    }
}

If the direct instance variable is false, Friend's doYourThing() method will simply invoke doYourThing() directly on the next reference. But if direct is true, doYourThing() will wrap the invocation of doYourThing() on the next reference in a doPrivileged() call. To do so, Friend instantiates an anonymous inner class that implements PrivilegedAction whose run() method invokes doYourThing() on next, and passes that object to doPrivileged().

To see Friend's doPrivileged() invocation in action, consider the Example2c application from the security/ex2 directory of the CD-ROM:

// On CD-ROM in file security/ex2/Example2c.java
import com.artima.security.friend.Friend;
import com.artima.security.stranger.Stranger;

// This succeeds because Friend code executes a
// doPrivileged() call. (Passing false as
// the second arg to Friend constructor causes
// it to do a doPrivileged().)

class Example2c {

    public static void main(String[] args) {

        TextFileDisplayer tfd = new TextFileDisplayer("answer.txt");

        Friend friend = new Friend(tfd, false);

        Stranger stranger = new Stranger(friend, true);

        stranger.doYourThing();
    }
}

Only one difference exists between the main() method of the Example2c application and the main() method of the previous example, Example2b. When the Example2b application instantiated the Friend object, it passed true as the second parameter. Example2c passes false. If you look back at the code for Friend (and Stranger) shown earlier in this chapter, you'll see that this parameter is used to decide whether to invoke doYourThing() directly on the Doer passed as the first parameter to the constructor. Because Example3c passes false, the Friend class will not invoke doYourThing() directly, but will invoke it indirectly via an AccessController.doPrivileged() invocation.

When the Example2c program invokes doYourThing() on the Stranger object referenced from the stranger variable, the Stranger object invokes doYourThing() on the Friend object, which (because direct is false) invokes doPrivileged(), passing in the anonymous inner class instance that implements PrivilegedAction. The doPrivileged() method invokes run() on the passed PrivilegedAction object, which invokes doYourThing() on the TextFileDisplayer object.

As in the previous example, TextFileDisplayer's doYourThing() method attempts to open and read a file named "answer.txt" in the current directory and print its contents to the standard output. When TextFileDisplayer's doYourThing() method creates a new FileReader object, the FileReader constructor creates a new FileInputStream, whose constructor checks to see whether or not a security manager has been installed. Once again, the concrete SecurityManager has been installed, so the FileInputStream's constructor invokes checkRead() on the concrete SecurityManager. The checkRead() method instantiates a new FilePermission object representing permission to read file answer.txt and passes that object to the concrete SecurityManager's checkPermission() method, which passes the object on to the checkPermission() method of the AccessController. The AccessController's checkPermission() method performs the stack inspection to determine whether this thread should be allowed to open file answer.txt for reading. The stack appears as shown in Figure 3-8.



Figure 3-8. Stack inspection for Example2c: stops at frame three.

The call stack to be inspected in Example2c looks similar to the call stacks inspected in Example2a and Example2b. The difference is that Example2c's call stack has two extra frames: frame four, which represents the doPrivileged() invocation, and frame five, which represents the run() invocation on the PrivilegedAction object. As always, stack inspection starts at the top of the stack and proceeds on down the stack towards frame one. But once again, the inspection process will not actually reach frame one. When the AccessController reaches frame four, it discovers a doPrivileged() invocation. As a result of this discovery, the AccessController makes one more check: it checks that the code represented by frame three, the code that invoked doPrivileged(), has permission to read answer.txt. Because frame three is associated with the FRIEND protection domain, that does have permission to read question.txt, the AccessController's checkPermission() method returns normally. Because the AccessController stopped its inspection at frame three, it never considered frame two, which because it is associated with the STRANGER protection domain, doesn't have permission to read answer.txt. Thus, by invoking doPrivileged() the Friend code was able to read file answer.txt, even though code beneath it on the call stack doesn't have permission to open the file.

To get the Example2c application to work as intended, you must, as with the previous examples, start the application with an appropriate command. When using the java program from the Java 2 SDK version 1.2, the appropriate command takes the form:

java -Djava.security.manager -Djava.security.policy=policyfile.txt
-Dcom.artima.ijvm.cdrom.home=d:\books\InsideJVM\manuscript\cdrom -cp
.;jars/friend.jar;jars/stranger.jar Example2c

This command, which is contained in the ex2c.bat file in the security/ex2 directory of the CD-ROM, is an example of the kind of command you'll need to use to get the example to work. As before, to execute Example2c on your own system, you must set the com.artima.ijvm.cdrom.home property to the security/ex2 directory of your CD-ROM, or to whatever directory you may have copied the security/ex2 directory from the CD-ROM. When you run this program, it should print out the contents of answer.txt:

Complexity threatens security to a significant extent. The more
complicated a security infrastructure becomes, the more likely
parties responsible for configuring security will either make
mistakes that open up security holes or avoid using the
security infrastructure altogether.

A Futile Use of doPrivileged()

It is important to understand that a method can never grant itself more privileges than it already has with a doPrivileged() invocation. By calling doPrivileged(), a method is merely enabling privileges it already has. It is telling the AccessController that it is taking responsibility for exercising its own permissions, and that the AccessController should ignore the permissions of its callers. Thus, the doPrivileged() call in the previous example, Example2c enabled answer.txt to be read because Friend, the class that executed the doPrivileged(), already had permission to read the file, and so did all the frames above it on the stack.

For an example of a futile attempt to use doPrivileged(), consider the Example2d application from the security/ex2 directory of the CD-ROM:

// On CD-ROM in file security/ex2/Example2d.java
import com.artima.security.friend.Friend;
import com.artima.security.stranger.Stranger;

// This fails because even though Stranger does
// a doPrivileged() call, Stranger doesn't have
// permission to read question.txt. (Passing
// false as second arg to Stranger constructor
// causes it to do a doPrivileged().)

class Example2d {

    public static void main(String[] args) {

        TextFileDisplayer tfd = new TextFileDisplayer("answer.txt");

        Stranger stranger = new Stranger(tfd, false);

        Friend friend = new Friend(stranger, true);

        friend.doYourThing();
    }
}

The difference between Example2d and the previous example, Example2c, is that the Stranger and Friend objects have swapped positions and roles. The Stranger object is now farther up the stack, with the Friend below it on the stack. And this time, it is Stranger that will make the call to doPrivileged(), not Friend.

When the Example2d program invokes doYourThing() on the Friend object referenced from the friend variable, the Friend object invokes doYourThing() on the Stranger object, which (because direct is false) invokes doPrivileged(), passing in the anonymous inner class instance that implements PrivilegedAction. The doPrivileged() method invokes run() on the passed PrivilegedAction object, which invokes doYourThing() on the TextFileDisplayer object.

As in the previous two examples, TextFileDisplayer's doYourThing() method attempts to open and read a file named "answer.txt" in the current directory and print its contents to the standard output. When TextFileDisplayer's doYourThing() method creates a new FileReader object, the FileReader constructor creates a new FileInputStream, whose constructor checks to see whether or not a security manager has been installed. As in all the examples, the concrete SecurityManager has been installed, so the FileInputStream's constructor invokes checkRead() on the concrete SecurityManager. The checkRead() method instantiates a new FilePermission object representing permission to read file answer.txt and passes that object to the concrete SecurityManager's checkPermission() method, which passes the object on to the checkPermission() method of the AccessController. The AccessController's checkPermission() method performs the stack inspection to determine whether this thread should be allowed to open file answer.txt for reading. The stack presented to the AccessController by Example2d is shown in Figure 3-9.



Figure 3-9. Stack inspection for Example2d: frame five doesn't have permission.

The call stack to be inspected in Example2d looks similar to the call stack inspected in Example2c. The only difference is that Friend and Stranger have swapped positions. As always, stack inspection starts at the top of the stack and proceeds on down the stack towards frame one. But alas, once again the inspection process will not actually reach frame one. When the AccessController reaches frame five, it discovers a stack frame associated with the STRANGER protection domain, which doesn't have permission to read answer.txt. As a result of this discovery, the AccessController throws an AccessControlException, indicating the requested read of answer.txt should not be performed.

Had the Stranger class been able to enlist the assistance of an instance of some class that implemented PrivilegedAction, performed the desired invocation of the TextFileDisplayer's doYourThing() method, and belonged to a protection domain that has permission to read >answer.txt, Stranger's attempt to open answer.txt with the help of doPrivileged() would have still been futile. Imagine, for example, that the code of the run() method represented by frame five of Example2d's call stack had been associated with to the CD-ROM protection domain. In that case, the AccessController would have determined that frame five had permission to open answer.txt and continued on to frame four. At frame four, the AccessController would have discovered the doPrivileged() invocation. As a result of this discovery, the AccessController would make one more check: it would make certain the method that invoked doPrivileged(), which in this case was Stranger's doYourThing() method represented by stack frame three, has permission to read file answer.txt. Because frame three is associated with the STRANGER protection domain that doesn't have permission to read answer.txt, the AccessController would still throw an AccessControlException.

To get the Example2d application to work as intended, you must start the application with yet another appropriate command. When using the java program from the Java 2 SDK version 1.2, the appropriate command takes the form:

java -Djava.security.manager -Djava.security.policy=policyfile.txt -
Dcom.artima.ijvm.cdrom.home=d:\books\InsideJVM\manuscript\cdrom -cp
.;jars/friend.jar;jars/stranger.jar Example2d

This command, which is contained in the ex2d.bat file in the security/ex2 directory of the CD-ROM, is an example of the kind of command you'll need to use to get the example to work. As before, to execute Example2d on your own system, you must set the com.artima.ijvm.cdrom.home property to the security/ex2 directory of your CD-ROM, or to whatever directory you may have copied the security/ex2 directory from the CD-ROM. When you run this program, you should see the kind of output that crackers everywhere hate to see:

Exception in thread "main" java.security.AccessControlException: access
denied (java.io.FilePermission answer.txt read)
	at java.security.AccessControlContext.checkPermission(AccessControlContext.java:195)
	at java.security.AccessController.checkPermission(AccessController.java:403)
	at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
	at java.lang.SecurityManager.checkRead(SecurityManager.java:873)
	at java.io.FileInputStream.(FileInputStream.java:65)
	at java.io.FileReader.(FileReader.java:35)
	at TextFileDisplayer.doYourThing(TextFileDisplayer.java, Compiled Code)
	at com.artima.security.stranger.Stranger$1.run(Stranger.java:27)
	at java.security.AccessController.doPrivileged(Native Method)
	at com.artima.security.stranger.Stranger.doYourThing(Stranger.java:24)
	at com.artima.security.friend.Friend.doYourThing(Friend.java:21)
	at Example2d.main(Example2d.java:21)

Missing Pieces and Future Directions

Java's security model, while far-reaching, does not address all potential threats posed by mobile code. For example, two potential activities of malicious mobile code that are not currently addressed by Java's security model are:

These kinds of attacks are called denial of service, because they deny the end-users from using their own computers. The Java security model does not currently offer ways to limit the usage of threads or memory by untrusted code. The difficulty in attempting to thwart this kind of hostile code is that it is hard to tell the difference, for example, between a hostile applet allocating a lot of memory and an image processing applet attempting to do useful work. Nevertheless, this kind of attack is a serious concern in certain situations, such as mission critical servers that run Java servlets.

Another area not currently incorporated into the security model is the idea of awarding permissions to principals on whose behalf code is being executed. A familiar example of this kind of access control is the UNIX operating system, which controls access to files based on a user ID that can only be obtained via an correct login name and password. As this kind of access control will be important in distributed systems such as those made possible by Jini, Sun is actively working to add this kind of user-centric security functionality to Java. The aim of the Java Authentication and Authorization Service (JAAS) is to enable access control to be based not just on the permissions granted to codebases and signers, but also on permissions granted to principals: the users who execute the code.

Security Beyond the Architecture

To be effective, a computer or network security strategy must be comprehensive. It cannot consist exclusively of a sandbox for running downloaded Java code. For instance, it may not matter much that the Java applets you download from the internet and run on your computer can't read the word processing file of your top-secret business plan if you:

In the context of a comprehensive security strategy, however, Java's security model can play a useful role.

The nice thing about Java's security model is that once you set it up, it does most of the work for you. You don't have to worry about whether a particular program is trusted or not--the Java runtime will determine that for you; and if it is untrusted, the Java runtime will protect your assets by encasing the untrusted code in a sandbox. The trouble is that, even though the designers of Java's security infrastructure did a good job of keeping things as simple as possible, the high degree functionality and flexibility offered by the security infrastructure demands a significant degree of complexity. As mentioned in the answer.txt file, which class Stranger so very much wanted to read in the AccessController examples given earlier in this chapter, complexity itself can represent a threat to security. The more complicated a security infrastructure becomes, the more likely parties responsible for configuring security will either make mistakes that open up security holes or avoid using the security infrastructure altogether.

End-users of Java software cannot rely solely on the security mechanisms built into Java's architecture. They must have a comprehensive security policy appropriate to their actual security requirements. Similarly, the security strategy of Java technology itself does not rely exclusively on the architectural security mechanisms described in this chapter. For example, one aspect of Java's security strategy is that anyone can sign a license agreement and get a copy of the source code of Sun's Java Platform implementation. Instead of keeping the internal implementation of Java's security architecture a secret "black box," it is open to anyone who wishes to look at it. This encourages security experts seeking a good technical challenge to try and find security holes in the implementation. When security holes are discovered, they can be patched. Thus, the openness of Java's internal implementation is part of Java's overall security strategy. Besides openness, there are several other aspects to Java's overall security strategy that don't directly involve its architecture. For more information about Java's overall security strategy, visit the resources page.

The Resources Page

For more information about Java and security, see the resources page: http://www.artima.com/insidejvm/resources/.


Sponsored Links

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use