Sponsored Link •
|
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.
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:
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.
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.
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:
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."
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.
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.
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.
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 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.
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 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:
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
.
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
.
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:
null
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 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:
checkConnect(String host, int port)
- open a socket
connection to the specified host and port number
checkConnect(String host, int port, Object
context)
- open a socket connection to the specified host and port number under the passed
security context
checkAccept(String host, int port)
- accept a socket connection from the specified host and
port number
checkCreateClassLoader()
- create a new class loader
checkAccess(Thread t)
- modify a thread (change its priority, stop it, etc...)
checkAccess(ThreadGroup t)
- modify a thread group (add a new thread, set deamonness, etc...)
checkExit()
- cause the application to exit
checkLink()
- load a dynamic library that contains native methods
checkRead(FileDescriptor fd)
- read from the specified file
checkRead(String file)
- read from the specified file
checkRead(String file, Object context)
- read from the
specified file under the passed security context
checkWrite(FileDescriptor fd)
- write to the specified file
checkWrite(String file)
- write to the specified file
checkDelete(String file)
- delete the specified file
checkListen(int port)
- wait for a connection on the specified
local port number
checkMulticast(InedAddress maddr)
- join, leave, send, or
receive IP multicast
checkMulticast(InedAddress maddr, byte ttl)
- join,
leave, send, or receive IP multicast
checkPropertiesAccess()
- access or modify system properties
in general
checkPropertiesAccess(String key)
- access or modify the
specified system property
checkTopLevelWindow(Object Window)
- bring up the
specified window without any warning
checkPrintJobAccess()
- initiate a print job request
checkSystemClipboardAccess()
- access the system clipboard
checkAWTEventQueueAccess()
- access the AWT event queue
checkPackageAccess(String pkg)
- access types from the
specified package (used by class loaders)
checkPackageDefinition(String pkg)
- add a new class to
the specified package (used by class loaders)
checkSetFactory()
- set the socket factory used by
ServerSocket
or Socket
or set the URL stream handler used by
URL
checkMemberAccess()
- access class information via the reflection
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()
:
checkPermission(Permission perm)
- take an action that
requires the specified permission
checkPermission(Permission perm, Object context)
- take an action that requires the specified permission under the passed security context
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
String
s "/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
.
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.
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.
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.
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.
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
String
s.
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.
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.
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
FilePermission
s 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
.
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.
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.
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.
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 When the Figure 3-6 shows the call stack when the
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
To get the This command, which is contained in the When the As an example of a stack inspection that results in a denied permission, consider the
The only difference between When the The call stack to be inspected in
To get the This command, which is contained in the The basic algorithm illustrated so far in this chapter, in which the
The basic 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 To enable trusted code to perform actions for which less trusted code farther down the call stack may
not have permission to do, the When you invoke 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
For an example of a If the To see Only one difference exists between the When the As in the previous example,
The call stack to be inspected in To get the This command, which is contained in the It is important to understand that a method can never grant itself more privileges than it already has
with a For an example of a futile attempt to use The difference between When the As in the previous two examples,
The call stack to be inspected in Had the To get the This command, which is contained in the 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.
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:
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
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.
For more information about Java and security, see the resources page:
Example2a
application creates three Doer
objects: a
TextFileDisplayer
, a Stranger
, and a
Friend
. The TextFileDisplayer
constructor is passed the
"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.
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.
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.
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.
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
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.
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"
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();
}
}
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"
.
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.
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.
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
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.
The
doPrivileged()
Method 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.
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.
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.
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()
.
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.
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.
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;
}
}
);
}
}
}
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()
.
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();
}
}
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.
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.
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.
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.
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
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()
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.
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();
}
}
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
.
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.
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.
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.
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
.
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
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.
Missing Pieces and Future Directions
Security Beyond the Architecture
In the context of a comprehensive security strategy, however, Java's security model can play a useful role.
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.
The Resources Page
http://www.artima.com/insidejvm/resources/
.
Sponsored Links
|