12.2. Signed Classes
One of the primary applications
of digital signatures in Java is to create and verify signed classes.
Signed classes allow the expansion of Java's sandbox in two
different ways:
-
The policy file can insist that classes coming from a particular site
be signed by a particular entity before the access controller will
grant that particular set of permissions. In the policy file, such an
entry contains a signedBy directive:
Class Definition
grant signedBy "sdo", codeBase "http://piccolo.East.Sun.COM/" {
java.io.FilePermission "-", "read,write";
}
This entry allows classes that are loaded from
piccolo.East.Sun.COM to read and write any local
files under the current directory only if the classes have been
signed by sdo.
-
The security manager can cooperate with the class loader in order to
determine whether or not a particular class is signed; the security
manager is then free to grant permissions to that class based on its
own internal policy. This technique is far more important in Java
1.1, since most Java 1.2 security managers simply defer decisions to
the access controller.
In this section, we'll explore the necessary components behind
this expansion of the Java sandbox. This example in the rest of the
section fills in the remaining details of the
JavaRunner program by showing us how to use a
signed class.
There are three necessary ingredients to expand the Java sandbox with
signed classes:
-
A method to create the signed class. The
jarsigner utility is used for this (see Appendix A, "Security Tools").
-
A class loader that knows how to understand the digital signature
associated with the class. The URLClassLoader
class knows how to do this, but we'll show an example of how to
do that for our JavaRunnerLoader class as well.
-
A security manager or access controller that grants the desired
permissions based on the digital signature. The default access
controller will do this for us; we'll show how the security
manager might do this directly.
12.2.1. Reading Signed JAR Files
Signed
classes in the Java-browser world are typically delivered as signed
JAR files; there are various tools (javakey for
Java 1.1 and jarsigner for Java 1.2) that can
take an ordinary JAR file and attach a digital signature to it. A
signed JAR file has three special elements:
-
A manifest (MANIFEST.MF), containing a
listing of the files in the archive that have been signed, along with
a message digest for each signed file.
-
A signature file (XXX.SF, where XXX is
the name of the entity that signed the archive) that contains
signature information. The data in this file is comprised of message
digests of entries in the manifest file.
-
A block file (XXX.DSA, where XXX is the
name of the entity that signed the archive and DSA is the name of the
signature algorithm used to create the signature). The block file
contains the actual signature data in a format known as PKCS7.
There are many advantages to this format, not the least of which is
that the
PKCS7 block file (that is, the signature itself) is a standard format
for external signatures. Unfortunately, the necessary classes to
create PKCS7 blocks are not part of Java's public API; if you
want to be able to write a signed JAR file, you'll need to
write the classes to create the signature block yourself.
However, we can read a signed JAR file using the core API. This means
that the class loader we've been using for the
JavaRunner program can be modified to read a
standard JAR file and associate the digital signature of that archive
with the classes it loads.
We'll enhance the JarLoader class loader
that we first developed in Chapter 3, "Java Class Loaders" in order to
read the signature. For reference, we'll show the entire class
again here, although only the highlighted portions of it have changed
(it also contains some methods that we added in Chapter 6, "Implementing Security Policies"):
Class Definition
public class JarLoader extends SecureClassLoader {
private URL urlBase;
public boolean printLoadMessages = true;
Hashtable classArrays;
Hashtable classIds;
static int groupNum = 0;
ThreadGroup threadGroup;
public JarLoader(String base, ClassLoader parent) {
super(parent);
try {
if (!(base.endsWith("/")))
base = base + "/";
urlBase = new URL(base);
classArrays = new Hashtable();
classIds = new Hashtable();
} catch (Exception e) {
throw new IllegalArgumentException(base);
}
}
private byte[] getClassBytes(InputStream is) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BufferedInputStream bis = new BufferedInputStream(is);
boolean eof = false;
while (!eof) {
try {
int i = bis.read();
if (i == -1)
eof = true;
else baos.write(i);
} catch (IOException e) {
return null;
}
}
return baos.toByteArray();
}
protected Class findClass(String name) {
String urlName = name.replace('.', '/');
byte buf[];
Class cl;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
int i = name.lastIndexOf('.');
if (i >= 0)
sm.checkPackageDefinition(name.substring(0, i));
}
buf = (byte[]) classArrays.get(urlName);
if (buf != null) {
Certificate ids[] = (Certificate) classIds.get(urlName);
CodeSource cs = new CodeSource(urlBase, ids);
cl = defineClass(name, buf, 0, buf.length, cs);
return cl;
}
try {
URL url = new URL(urlBase, urlName + ".class");
if (printLoadMessages)
System.out.println("Loading " + url);
InputStream is = url.openConnection().getInputStream();
buf = getClassBytes(is);
CodeSource cs = new CodeSource(urlBase, null);
cl = defineClass(name, buf, 0, buf.length, cs);
return cl;
} catch (Exception e) {
System.out.println("Can't load " + name + ": " + e);
return null;
}
}
public void readJarFile(String name) {
URL jarUrl = null;
JarInputStream jis;
JarEntry je;
try {
jarUrl = new URL(urlBase, name);
} catch (MalformedURLException mue) {
System.out.println("Unknown jar file " + name);
return;
}
if (printLoadMessages)
System.out.println("Loading jar file " + jarUrl);
try {
jis = new JarInputStream(
jarUrl.openConnection().getInputStream());
} catch (IOException ioe) {
System.out.println("Can't open jar file " + jarUrl);
return;
}
try {
while ((je = jis.getNextJarEntry()) != null) {
String jarName = je.getName();
if (jarName.endsWith(".class"))
loadClassBytes(jis, jarName, je);
// else ignore it; it could be an image or audio file
jis.closeEntry();
}
} catch (IOException ioe) {
System.out.println("Badly formatted jar file");
}
}
private void loadClassBytes(JarInputStream jis,
String jarName, JarEntry je) {
if (printLoadMessages)
System.out.println("\t" + jarName);
BufferedInputStream jarBuf = new BufferedInputStream(jis);
ByteArrayOutputStream jarOut = new ByteArrayOutputStream();
int b;
try {
while ((b = jarBuf.read()) != -1)
jarOut.write(b);
String className = jarName.substring(0, jarName.length() - 6);
classArrays.put(className, jarOut.toByteArray());
Certificate c[] = je.getCertificates();
if (c == null)
c = new Certificate[0];
classIds.put(className, c);
} catch (IOException ioe) {
System.out.println("Error reading entry " + jarName);
}
}
public void checkPackageAccess(String name) {
SecurityManager sm = System.getSecurityManager();
if (sm != null)
sm.checkPackageAccess(name);
}
ThreadGroup getThreadGroup() {
if (threadGroup == null)
threadGroup = new ThreadGroup(
"JavaRuner ThreadGroup-" + groupNum++);
return threadGroup;
}
String getHost() {
return urlBase.getHost();
}
}
Interestingly enough, all the details of the digital signature are
handled for us by the classes in the jar
package. All that we're left to do is obtain the array of
signers when we read in each JAR entry and then use that array of
signers when we construct the code source we use to define the class.
Remember that each file in a JAR file may be signed by a different
group of identities and that some may not be signed at all. This is
why we must construct a new code source object for each signed class
that was in the JAR file.
12.2.2. The Signed JAR File and Security Policies
The last item in our examination of signed JAR files involves the
security policy and its interaction with the signed JAR file. In the
case where the security policy is completely determined by the access
controller, the class loader has already done all our work for us;
the access controller depends on each class to have an appropriate
code source, and permissions for that code will be completely defined
in the policy file.
In Java 1.1, the mechanism is different; we can't use the JAR
classes to parse a signed JAR file, and we can't use the
defineClass() method to set the signers for a
particular signed class. The first of these problems is harder to
overcome; it requires that you implement the equivalent of the
java.util.jar package. We've presented all
the background information you'd need to do that, but it is a
lot of code to write (so we won't). The second of these
problems means that your class loader must define a class as follows:
Class Definition
if (isSecure(urlName)) {
cl = defineClass(name, buf, 0, buf.length);
if (ids != null)
setSigners(cl, ids);
}
else cl = defineClass(name, buf, 0, buf.length);
The isSecure() method in this case must base its
decision on information obtained from reading the manifest of the JAR
file and verifying the signature that is contained in the signature
file. The array of ids will need to be created
by constructing instances of the Identity class
to represent the signer of the class.
The reason for setting the signers in this way is to allow the
security manager to retrieve those
signatures easily. When the security manager does not defer all
permissions to the access controller--and, hence, in all Java
1.1 programs--the security manager will need to take advantage
of signed class information to base its decisions. This is typically
done by programming the security manager to retrieve the keys that
were used to sign a class via the getSigners()
method. This allows the security manger to function with any standard
signature-aware class loader. The security manager could then do
something like this:
Class Definition
public void checkAccess(Thread t) {
Class cl = currentLoadedClass();
if (cl == null)
return;
Identity ids[] = (Identity[]) cl.getSigners();
for (int i = 0; i < ids.length; i++) {
if (isTrustedId(ids[i]))
return;
}
throw new SecurityException("Can't modify thread states");
}
The key to this example is writing a good
isTrustedId() method. A possible implementation
is to use the information stored in the keystore (for 1.2) or
identity database (for 1.1) to grant a level of trust to an entity;
such an implementation requires that you have a non-default
implementation of these databases. Alternately, your application
could hardwire the public keys of certain entities (like the public
key of the HR group of XYZ corporation) and use that information as
the basis of its security decisions.
 |  |  |
| 12.1. The Signature Class |  | 12.3. Implementing a Signature Class |

Copyright © 2001 O'Reilly & Associates. All rights reserved.
|