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:
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.
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"):
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.
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:
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:
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.
Copyright © 2001 O'Reilly & Associates. All rights reserved.
This HTML Help has been published using the chm2web software. |