Programmatically Signing JAR Files
Pages: 1, 2, 3
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 );