ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Programmatically Signing JAR Files

by Raffi Krikorian
04/12/2001
  Related Reading:
Java Security, 2nd Edition

Java Security, 2nd Edition
By Scott Oaks
2nd Edition May 2001 (est.)
0-596-00157-6, Order Number: 1576
550 pages (est.), $39.95 (est.)

The jarsigner tool provides a way for developers to sign their JAR files with a given private key, so that others may verify the classes provided inside the archive. Unfortunately, neither this tool, nor the underlying sun.tools.jar.Main, is appropriate for embedding in other applications; in order to allow programmatic JAR signing, jarsigner needs to be retooled.

It is not an accident that programmatic JAR signing isn't supported by the core Java library. JAR signing gives users the ability to verify the integrity of the classes and other resources contained within the archive. Most JAR files are signed with the private key of the archive author, which is the end of a certificate chain that begins at a well-known certificate authority. Standard private key usage dictates that the password protecting a private key be kept only by the code author; storing the password on a computer makes the password inherently insecure. Thus, in most cases, programmatically signing JAR files is a frowned upon; however, there are a few cases when it is necessary.

Brief Primer on the Signed JAR Format

Signed JAR files are simply JAR files with a few additional files that Java uses for verification. Using public-private encryption, Java appends each file in the JAR to a buffer, and signs each file individually. Java then records these entries into the Manifest file. This Manifest is converted into a signature file which contains a digest of the entire manifest, preventing any files from being added or deleted from the JAR. The public key used for verification is provided along with the JAR file.

These additional files are inserted into the META-INF JAR entry directory of the signed JAR file. The signature block file has a SF extension and is named according to the key used to sign. (The jarsigner converts the proposed name for the key alias to be the name of the signing key; all alphanumeric characters in the name become uppercase; all other characters are replaced with the _ character.) The public key is also inserted with a DSA extension. The manifest file is then inserted as MANIFEST.MF.

The signed JAR format verifies that the user of the JAR file has received the file in the state that the signer intended. The JAR user can also decide whether he or she trusts the JAR signer or any other entity along the certificate chain. Signature blocks are computed by incrementally updating the signing buffer. This prevents files in the JAR file from being altered; a single modification would effect a change in the signature blocks for that file and for every file following it. This also ensures that files are not inserted "between" any of the files in the Manifest. Since the signature of the entire Manifest is also provided, the recipient can be sure that no files have been added to the end of the JAR file.

During verification, the public key is first extracted from the signed JAR file. Next, the Manifest signature is checked to ensure that no entity has tampered with the Manifest. The contents of the JAR are then compared to the contents and the attributes in the Manifest; all the files in the JAR must be in the Manifest (and all entries in the Manifest must be present in the JAR), and the signatures blocks provided in the Manifest must verify. If any of these steps fail, then the JAR isn't verified correctly.

jarsigner

The JDK provides jarsigner as the tool to sign and verify JAR files. When jarsigner is given an existing JAR file and a private key, it signs each file in the archive, records all the necessary information into the files detailed above, and outputs a signed JAR. When used in verification mode, jarsigner does the reverse, verifying the signatures provided in the signature block file and in the manifest against the public key provided in the META-INF directory of the JAR.

The drawback to jarsigner is that was created for use by a person; it was not designed to be embedded in any other application. Simply forking off the jarsigner executable from within another process requires that the contents of the process's System.out and System.err stream be parsed to account for all errors. Because many different errors are wrapped in the same error message, not all errors are reported distinctly via the error stream. Making use of the classes in the sun.tools.jar package, which contains all classes comprising the jarsigner, doesn't help, as the sun.tools.jar.Main class reports its error via two java.io.PrintStreams and a boolean return value on the only extra-package accessible method.

The ideal JAR signing solution would allow for all the different errors to be distinguished in a programmatic way and not by parsing strings off a PrintStream. To provide this, the JAR signing utility class must not simply return true or false on whether the method completes properly, but, instead, it should propagate the different exceptions which may occur when signing the JAR file. This allows other authors to catch the different exceptions and handle them as desired.

JARSigner

Now that we have described an ideal JAR signing utility, let's create one. The following JARSigner class takes a name for the signing key, a java.security.PrivateKey, and an array of java.security.cert.X509Certificate objects which act as the certificate chain, the first element of which is the certificate containing the public key corresponding to the private key used in the JAR signing, and which ends in the certificate of the root signing authority. We then provide a signJarFile method which takes as parameters the java.util.jar.JarFile to sign and an output stream to which the signed jar file will be written. The signing method, as desired, also propagates java.security.NoSuchAlgorithmException, java.security.InvalidKeyException, java.security.SignatureException, and java.io.IOException -- all of which can be handled individually by any application which wishes to wrap the JARSigner.

import java.io.*;
import java.security.*;
import java.security.cert.*;
import java.util.*;
import sun.misc.BASE64Encoder;
import sun.security.util.ManifestDigester;
import sun.security.util.SignatureFile;

public class JARSigner
extends Object {
  // the alias for the signing key, the private key to sign with,
  // and the certificate chain
  private String alias;
  private PrivateKey privateKey;
  private X509Certificate[] certChain;

  public JARSigner( String alias, PrivateKey privateKey, X509Certificate[] certChain ) {
    this.alias = alias;
    this.privateKey = privateKey;
    this.certChain = certChain;

  }

Getting a valid Manifest

If a JAR has a MANIFEST.MF file, then we need to extract it from the JAR and verify its contents. All entries must be validated; entries in the Manifest that do not map to actual entries in the JAR must be removed and JAR entries that do not have corresponding entries in the Manifest must be added. To do this, three methods are provided: one to extract a Manifest if one exists, another to validate the contents, and a third, a higher level method, to create both the Manifest and to verify the contents.

// retrieve the manifest from a jar file -- this will either
// load a pre-existing META-INF/MANIFEST.MF, or create a new
// one
private static Manifest getManifestFile( JarFile jarFile )
throws IOException {
JarEntry je = jarFile.getJarEntry( "META-INF/MANIFEST.MF" );
if( je != null ) {
Enumeration entries = jarFile.entries();
while( entries.hasMoreElements() ) {
je = (JarEntry)entries.nextElement();
if( "META-INF/MANIFEST.MF".equalsIgnoreCase( je.getName() ) )
break;

else
je = null;

}

}

// create the manifest object
Manifest manifest = new Manifest();
if( je != null )
manifest.read( jarFile.getInputStream( je ) );
return manifest;

}

// given a manifest file and given a jar file, make sure that
// the contents of the manifest file is correct and return a
// map of all the valid entries from the manifest
private static Map pruneManifest( Manifest manifest, JarFile jarFile )
throws IOException {
Map map = manifest.getEntries();
Iterator elements = map.keySet().iterator();
while( elements.hasNext() ) {
String element = (String)elements.next();
if( jarFile.getEntry( element ) == null )
elements.remove();

}
return map;

}

// make sure that the manifest entries are ready for the signed
// JAR manifest file. if we already have a manifest, then we
// make sure that all the elements are valid. if we do not
// have a manifest, then we create a new signed JAR manifest
// file by adding the appropriate headers
private static Map createEntries( Manifest manifest, JarFile jarFile )
throws IOException {
Map entries = null;
if( manifest.getEntries().size() > 0 )
entries = pruneManifest( manifest, jarFile );

else {
// if there are no pre-existing entries in the manifest,
// then we put a few default ones in
Attributes attributes = manifest.getMainAttributes();
attributes.putValue( Attributes.Name.MANIFEST_VERSION.toString(), "1.0" );
attributes.putValue( "Created-By", System.getProperty( "java.version" ) + " (" + System.getProperty( "java.vendor" ) + ")" );
entries = manifest.getEntries();

}
return entries;

}

Inserting file signatures into the Manifest

Each entry must have an associated cryptographic message digest in the Manifest. As described above, we need to enumerate through all the JAR entries and record the base-64 encoding of the signature of the "running" contents of the JAR, i.e. the signature of the first file is the digest of the contents of the first entry, the signature of the second file is the digest of the contents of the second file appended to the first, and so on. The digest is recorded as an attribute under the key SHA1-Digest for the entry in the Manifest.

Here is an example of a Manifest for a signed JAR

Manifest-Version: 1.0
Created-By: 1.3.0_02 (Sun Microsystems Inc.)

Name: a/b/c1.class
SHA1-Digest: fcav7ShIG6i86xPepmitOVo4vWY=

Name: a/b/c2.class
SHA1-Digest: kdHbE7kL9ZHLgK7akHttYV4XIa0=

The SHA1-Digest for a/b/c1.class is the base-64 version of the signature block of c1.class, and the SHA1-Digest of a/b/c2.class is the signature block of the bytes of c1.class and c2.class. The files in the META-INF directory do not make it into the Manifest.

// helper function to update the digest
private static BASE64Encoder b64Encoder = new BASE64Encoder();
private static String updateDigest( MessageDigest digest, InputStream inputStream )
throws IOException {
  byte[] buffer = new byte[2048];
  int read = 0;
  while( ( read = inputStream.read( buffer ) ) > 0 )
  digest.update( buffer, 0, read );
  inputStream.close();

  return b64Encoder.encode( digest.digest() );

}

// update the attributes in the manifest to have the
// appropriate message digests. we store the new entries into
// the entries Map and return it (we do not compute the digests
// for those entries in the META-INF directory)
private static Map updateManifestEntries( Manifest manifest, JarFile jarFile, MessageDigest messageDigest, Map entries )
throws IOException {
  Enumeration jarElements = jarFile.entries();
  while( jarElements.hasMoreElements() ) {
    JarEntry jarEntry = (JarEntry)jarElements.nextElement();
    if( jarEntry.getName().startsWith( "META-INF" ) )
    continue;

    else if( manifest.getAttributes( jarEntry.getName() ) != null ) {
    // update the digest and record the base 64 version of
    // it into the attribute list
    Attributes attributes = manifest.getAttributes( jarEntry.getName() );
    attributes.putValue( "SHA1-Digest", updateDigest( messageDigest, jarFile.getInputStream( jarEntry ) ) );

    } else if( !jarEntry.isDirectory() ) {
    // store away the digest into a new Attribute
    // because we don't already have an attribute list
    // for this entry. we do not store attributes for
    // directories within the JAR
    Attributes attributes = new Attributes();
    attributes.putValue( "SHA1-Digest", updateDigest( messageDigest, jarFile.getInputStream( jarEntry ) ) );
    entries.put( jarEntry.getName(), attributes );

    }

  }
  return entries;

}

Creating the signature and signature block file

The signature file is the record that contains the signed version of the Manifest file, preventing any files from being added to the JAR file without the signed JAR verifier noticing. The signature file contains a digest of both the entire Manifest in the SHA1-Digest-Manifest header as well as digest values for all the entries that are present in the Manifest. It is similar to the Manifest, except taht the digests are computed from the corresponding values in the Manifest rather than the contents of the file itself.

Signature-Version: 1.0
SHA1-Digest-Manifest: h1yS+K9T7DyHtZrtI+LxvgqaMYM=
Created-By: SignatureFile JDK 1.2

Name: a/b/c1.class
SHA1-Digest: fcav7ShIG6i86xPepmitOVo4vWY=

Name: a/b/c2.class
SHA1-Digest: xrQem9snnPhLySDiZyclMlsFdtM=

This is an example of the final SF file in the META-INF directory of the signed JAR. Thankfully, there are already classes in the Java library to generate the signature file for us, so we do not have to manually create it. Using the sun.security.util package, we can easily use a serialized version of the Manifest along with a ManifestDigester and a SignatureFile object. The SignatureFile object can write itself to an output stream, so we can use that later to write the file out into the JAR.

// a small helper function that will convert a manifest into an
// array of bytes
private byte[] serialiseManifest( Manifest manifest )
throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
manifest.write( baos );
baos.flush();
baos.close();
return baos.toByteArray();

}

// create a signature file object out of the manifest and the
// message digest
private SignatureFile createSignatureFile( Manifest manifest, MessageDigest messageDigest )
throws IOException {
// construct the signature file and the signature block for
// this manifest
ManifestDigester manifestDigester = new ManifestDigester( serialiseManifest( manifest ) );
return new SignatureFile( new MessageDigest[] { messageDigest }, manifest, manifestDigester, this.alias, true );

}

A signature block must be created along with the signature. It contains the public key and the certificate signing chain in a non-human readable format used to verify the contents of the signed JAR. Using the sun.security.util package, we can generate the signature block from the SignatureFile object that we just created.

SignatureFile.Block block = signatureFile.generateBlock( this.privateKey, this.certChain, true );

Creating the signed JAR

Now we can actually create and output the signed JAR file. The signJarFile method is the publicly accessible method which propagates all the exceptions which may occur when signing the JAR file. First we need to compute all the necessary files to go into the META-INF directory for the signed JAR. Next we write out the contents of the META-INF directory (the MANIFEST.MF file, the SF, the DSA file, and the rest of the files that were originally in the META-INF directory), and then we will iterate, writing out the rest of the JAR.

// a helper function that can take entries from one jar file and
// write it to another jar stream
private static void writeJarEntry( JarEntry je, JarFile jarFile, JarOutputStream jos )
throws IOException {
jos.putNextEntry( je );
byte[] buffer = new byte[2048];
int read = 0;
InputStream is = jarFile.getInputStream( je );
while( ( read = is.read( buffer ) ) > 0 )
jos.write( buffer, 0, read );
jos.closeEntry();

}

// the actual JAR signing method -- this is the method which
// will be called by those wrapping the JARSigner class
public void signJarFile( JarFile jarFile, OutputStream outputStream )
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, IOException {

// calculate the necessary files for the signed jAR

// get the manifest out of the jar and verify that
// all the entries in the manifest are correct
Manifest manifest = getManifestFile( jarFile );
Map entries = createEntries( manifest, jarFile );

// create the message digest and start updating the
// the attributes in the manifest to contain the SHA1
// digests
MessageDigest messageDigest = MessageDigest.getInstance( "SHA1" );
updateManifestDigest( manifest, jarFile, messageDigest, entries );

// construct the signature file object and the
// signature block objects
SignatureFile signatureFile = createSignatureFile( manifest, messageDigest );
SignatureFile.Block block = signatureFile.generateBlock( privateKey, certChain, true );


// start writing out the signed JAR file

// write out the manifest to the output jar stream
String manifestFileName = "META-INF/MANIFEST.MF";
JarOutputStream jos = new JarOutputStream( outputStream );
JarEntry manifestFile = new JarEntry( manifestFileName );
jos.putNextEntry( manifestFile );
jos.write( manifestBytes, 0, manifestBytes.length );
jos.closeEntry();

// write out the signature file -- the signatureFile
// object will name itself appropriately
String signatureFileName = signatureFile.getMetaName();
JarEntry signatureFileEntry = new JarEntry( signatureFileName );
jos.putNextEntry( signatureFileEntry );
signatureFile.write( jos );
jos.closeEntry();

// write out the signature block file -- again, the block
// will name itself appropriately
String signatureBlockName = block.getMetaName();
JarEntry signatureBlockEntry = new JarEntry( signatureBlockName );
jos.putNextEntry( signatureBlockEntry );
block.write( jos );
jos.closeEntry();

// commit the rest of the original entries in the
// META-INF directory. if any of their names conflict
// with one that we created for the signed JAR file, then
// we simply ignore it
Enumeration metaEntries = jarFile.entries();
while( metaEntries.hasMoreElements() ) {
JarEntry metaEntry = (JarEntry)metaEntries.nextElement();
if( metaEntry.getName().startsWith( "META-INF" ) &&
!( manifestFileName.equalsIgnoreCase( metaEntry.getName() ) ||
signatureFileName.equalsIgnoreCase( metaEntry.getName() ) ||
signatureBlockName.equalsIgnoreCase( metaEntry.getName() ) ) )
writeJarEntry( metaEntry, jarFile, jos );

}

// now write out the rest of the files to the stream
Enumeration allEntries = jarFile.entries();
while( allEntries.hasMoreElements() ) {
JarEntry entry = (JarEntry)allEntries.nextElement();
if( !entry.getName().startsWith( "META-INF" ) )
writeJarEntry( entry, jarFile, jos );

}

// finish the stream that we have been writing to
jos.flush();
jos.finish();

// close the JAR file that we have been using
jarFile.close();

}

}

And that's it. Using this class, JAR files can be signed from within different applications; their error conditions are exposed so they may be caught and dealt with in a more robust way than that which is provided by the Java libraries themselves.

Raffi Krikorian makes a career of hacking everything and anything. Professionally, he is the founding partner at Synthesis Studios: a technological design and consulting firm that orchestrates his disjointed train of thought.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.