Digitally sign the contents of a WAR file

We recently upgraded our web application’s technology stack to JAVA 7 (u45) and were faced by numerous security warnings posted by JAVA 7 on usage of unsigned applets. To overcome the same, we had a requirement where before deploying a war file on a production environment we were needed to sign the jar files present inside it in order to avoid the security warning messages when the applet jars were downloaded and executed on the client machines.

Our web application is configured on Maven 2 to build the necessary wars and deploy on Tomcat. So there were multiple approaches to be considered while thinking about the jar signing issues like do it via Maven itself or sign the war externally.

In this post I am going to explain the way I adopted to sign the war file externally using JDK’s JarSigner utility.

Here is the snippet I wrote in order to –

  • Unpack a war file,
  • Sign the jars included in it
  • Pack the contents back into a war again
  • JarSignerUtility.java

    package jar.signer.tools;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.Enumeration;
    import java.util.Properties;
    import java.util.jar.JarEntry;
    import java.util.jar.JarFile;
    
    import org.apache.commons.io.FileUtils;
    import org.apache.commons.lang.StringUtils;
    
    import sun.security.tools.JarSigner;
    
    public class JarSignerUtility {
    
    	public static String UNSIGNED_WARS = "UNSIGNED_WARS";
    
    	public static String DESTINATION_DIRECTORY_PATH = "DESTINATION_DIRECTORY_PATH";
    
    	public static String SIGNED_WARS = "SIGNED_WARS";
    
    	public static String KEYSTORE_PATH = "KEYSTORE_PATH";
    
    	public static String ALIAS = "ALIAS";
    
    	public static String STORE_TYPE = "STORE_TYPE";
    
    	public static String STORE_PASSWORD = "STORE_PASSWORD";
    
    	private static Properties prop = new Properties();
    
    	public static void main(String[] args) {
    		try {
    			prop.load(new FileInputStream("./jarSignProperties.properties"));
    			String[] unsignedWarFiles = StringUtils.split(prop.getProperty(UNSIGNED_WARS), ",");
    			String[] signedWarFiles = StringUtils.split(prop.getProperty(SIGNED_WARS), ",");
    			String destination = prop.getProperty(DESTINATION_DIRECTORY_PATH);
    			destination = StringUtils.isEmpty(destination) ? "./temp/" : destination;
    			for (int i = 0; i < unsignedWarFiles.length; i++) {
    				String fileName = unsignedWarFiles[i];
    				System.out.println("Signing " + (i+1) +". "  + fileName);
    				JarFile warfile = new JarFile(new File(fileName));
    				Enumeration enu = warfile.entries();
    				while (enu.hasMoreElements()) {
    					JarEntry je = enu.nextElement();
    					File fl = new File(destination, je.getName());
    					if (!fl.exists()) {
    						fl.getParentFile().mkdirs();
    						fl = new File(destination, je.getName());
    					}
    					if (je.isDirectory()) {
    						continue;
    					}
    					InputStream is = warfile.getInputStream(je);
    					FileOutputStream fo = new FileOutputStream(fl);
    					while (is.available() > 0) {
    						fo.write(is.read());
    					}
    					fo.close();
    					is.close();
    					if (fl.getPath().endsWith("jar")) { // Sign the jar files within the war
    						System.out.println("\t" + fl.getPath());
    						JarSigner jarSigner = new JarSigner();
    						jarSigner.run(new String[] { "-keystore", prop.getProperty(KEYSTORE_PATH), "-storepass",
    								prop.getProperty(STORE_PASSWORD), "-storetype", prop.getProperty(STORE_TYPE),
    								fl.getPath(), prop.getProperty(ALIAS) });
    					}
    				}
    				packWar(destination,signedWarFiles[i]);
    				File destinationDir = new File(destination);
    				if(destinationDir.exists()){
    					FileUtils.deleteDirectory(destinationDir);
    				}
    			}
    		} catch (IOException e) {
    			System.out.println("Exception occured : " + e.getMessage());
    			e.printStackTrace();
    		}
    	}
    
    	/**
    	 * Packs the contents of destination directory back to .war file
    	 * 
    	 * @param signedWarFile
    	 * @param signedWarFiles 
    	 * 
    	 * @throws IOException
    	 */
    	public static void packWar(String destination, String signedWarFile) throws IOException {
    		System.out.println("** Packaging " + signedWarFile +"\n\n");
    		JarToolSet.jar(new File(destination), new File(signedWarFile), true);
    	}
    }
    
    

    The JarToolSet java class referred above has the following contents –
    JarToolSet.java

    package jar.signer.tools;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.util.Enumeration;
    import java.util.HashMap;
    import java.util.jar.JarEntry;
    import java.util.jar.JarFile;
    import java.util.jar.JarInputStream;
    import java.util.jar.JarOutputStream;
    import java.util.zip.CRC32;
    import java.util.zip.ZipFile;
    import java.util.zip.ZipOutputStream;
    
    /**
     * 
     * @author http://www.java2s.com/Code/Java/File-Input-Output/Writesallfilesofthegivendirectorytothespecifiedjarfile.htm
     * 
     */
    public final class JarToolSet {
    
    	private final static boolean IS_WINDOWS = (File.separatorChar == '\\');
    
    	/**
    	 * Writes all files of the given directory to the specified jar-file.
    	 * 
    	 * @param sourceDir
    	 *            The directory containing the "source" files.
    	 * @param target
    	 *            The jar file which should be created
    	 * @param compress
    	 *            True when the jar file should be compressed
    	 * @throws FileNotFoundException
    	 *             when a file could not be found
    	 * @throws IOException
    	 *             when a file could not be read or the jar file could not be written to.
    	 */
    	public static void jar(File sourceDir, File target, boolean compress) throws IOException {
    		jar(sourceDir, new FileOutputStream(target), compress);
    	}
    
    	/**
    	 * Writes all given files to the specified jar-file.
    	 * 
    	 * @param files
    	 *            all files that should be added to the JAR file
    	 * @param sourceDir
    	 *            The parent directory containing the given files.
    	 * @param target
    	 *            The jar file which should be created
    	 * @param compress
    	 *            True when the jar file should be compressed
    	 * @throws FileNotFoundException
    	 *             when a file could not be found
    	 * @throws IOException
    	 *             when a file could not be read or the jar file could not be written to.
    	 */
    	public static void jar(File sourceDir, OutputStream target, boolean compress) throws IOException {
    		File[] files = sourceDir.listFiles();
    		// creates target-jar-file:
    		JarOutputStream out = new JarOutputStream(target);
    		if (compress) {
    			out.setLevel(ZipOutputStream.DEFLATED);
    		} else {
    			out.setLevel(ZipOutputStream.STORED);
    		}
    		// create a CRC32 object:
    		CRC32 crc = new CRC32();
    		byte[] buffer = new byte[1024 * 1024];
    		// add all files:
    		int sourceDirLength = sourceDir.getAbsolutePath().length() + 1;
    		for (File file : files) {
    			addFile(file, out, crc, sourceDirLength, buffer);
    		}
    		out.close();
    	}
    
    	/**
    	 * Adds one file to the given jar file. If the specified file is a directory, all included files will be added.
    	 * 
    	 * @param file
    	 *            The file which should be added
    	 * @param out
    	 *            The jar file to which the given jar file should be added
    	 * @param crc
    	 *            A helper class for the CRC32 calculation
    	 * @param sourceDirLength
    	 *            The number of chars which can be skipped from the file's path
    	 * @param buffer
    	 *            A buffer for reading the files.
    	 * @throws FileNotFoundException
    	 *             when the file was not found
    	 * @throws IOException
    	 *             when the file could not be read or not be added
    	 */
    	private static void addFile(File file, JarOutputStream out, CRC32 crc, int sourceDirLength, byte[] buffer)
    			throws FileNotFoundException, IOException {
    		if (file.isDirectory()) {
    			File[] fileNames = file.listFiles();
    			for (int i = 0; i < fileNames.length; i++) {
    				addFile(fileNames[i], out, crc, sourceDirLength, buffer);
    			}
    		} else {
    			String entryName = file.getAbsolutePath().substring(sourceDirLength);
    			if (IS_WINDOWS) {
    				entryName = entryName.replace('\\', '/');
    			}
    			JarEntry entry = new JarEntry(entryName);
    			// read file:
    			FileInputStream in = new FileInputStream(file);
    			add(entry, in, out, crc, buffer);
    		}
    	}
    
    	/**
    	 * @param entry
    	 * @param in
    	 * @param out
    	 * @param crc
    	 * @param buffer
    	 * @throws IOException
    	 */
    	private static void add(JarEntry entry, InputStream in, JarOutputStream out, CRC32 crc, byte[] buffer)
    			throws IOException {
    		out.putNextEntry(entry);
    		int read;
    		long size = 0;
    		while ((read = in.read(buffer)) != -1) {
    			crc.update(buffer, 0, read);
    			out.write(buffer, 0, read);
    			size += read;
    		}
    		entry.setCrc(crc.getValue());
    		entry.setSize(size);
    		in.close();
    		out.closeEntry();
    		crc.reset();
    	}
    
    	/**
    	 * Adds the given file to the specified JAR file.
    	 * 
    	 * @param file
    	 *            the file that should be added
    	 * @param jarFile
    	 *            The JAR to which the file should be added
    	 * @param parentDir
    	 *            the parent directory of the file, this is used to calculate the path witin the JAR file. When null is
    	 *            given, the file will be added into the root of the JAR.
    	 * @param compress
    	 *            True when the jar file should be compressed
    	 * @throws FileNotFoundException
    	 *             when the jarFile does not exist
    	 * @throws IOException
    	 *             when a file could not be written or the jar-file could not read.
    	 */
    	public static void addToJar(File file, File jarFile, File parentDir, boolean compress)
    			throws FileNotFoundException, IOException {
    		File tmpJarFile = File.createTempFile("tmp", ".jar", jarFile.getParentFile());
    		JarOutputStream out = new JarOutputStream(new FileOutputStream(tmpJarFile));
    		if (compress) {
    			out.setLevel(ZipOutputStream.DEFLATED);
    		} else {
    			out.setLevel(ZipOutputStream.STORED);
    		}
    		// copy contents of old jar to new jar:
    		JarFile inputFile = new JarFile(jarFile);
    		JarInputStream in = new JarInputStream(new FileInputStream(jarFile));
    		CRC32 crc = new CRC32();
    		byte[] buffer = new byte[512 * 1024];
    		JarEntry entry = (JarEntry) in.getNextEntry();
    		while (entry != null) {
    			InputStream entryIn = inputFile.getInputStream(entry);
    			add(entry, entryIn, out, crc, buffer);
    			entryIn.close();
    			entry = (JarEntry) in.getNextEntry();
    		}
    		in.close();
    		inputFile.close();
    
    		int sourceDirLength;
    		if (parentDir == null) {
    			sourceDirLength = file.getAbsolutePath().lastIndexOf(File.separatorChar) + 1;
    		} else {
    			sourceDirLength = file.getAbsolutePath().lastIndexOf(File.separatorChar) + 1
    					- parentDir.getAbsolutePath().length();
    		}
    		addFile(file, out, crc, sourceDirLength, buffer);
    		out.close();
    
    		// remove old jar file and rename temp file to old one:
    		if (jarFile.delete()) {
    			if (!tmpJarFile.renameTo(jarFile)) {
    				throw new IOException("Unable to rename temporary JAR file to [" + jarFile.getAbsolutePath() + "].");
    			}
    		} else {
    			throw new IOException("Unable to delete old JAR file [" + jarFile.getAbsolutePath() + "].");
    		}
    
    	}
    
    	/**
    	 * Extracts the given jar-file to the specified directory. The target directory will be cleaned before the jar-file
    	 * will be extracted.
    	 * 
    	 * @param jarFile
    	 *            The jar file which should be unpacked
    	 * @param targetDir
    	 *            The directory to which the jar-content should be extracted.
    	 * @throws FileNotFoundException
    	 *             when the jarFile does not exist
    	 * @throws IOException
    	 *             when a file could not be written or the jar-file could not read.
    	 */
    	public static void unjar(File jarFile, File targetDir) throws FileNotFoundException, IOException {
    		// clear target directory:
    		if (targetDir.exists()) {
    			targetDir.delete();
    		}
    		// create new target directory:
    		targetDir.mkdirs();
    		// read jar-file:
    		String targetPath = targetDir.getAbsolutePath() + File.separatorChar;
    		byte[] buffer = new byte[1024 * 1024];
    		JarFile input = new JarFile(jarFile, false, ZipFile.OPEN_READ);
    		Enumeration enumeration = input.entries();
    		for (; enumeration.hasMoreElements();) {
    			JarEntry entry = enumeration.nextElement();
    			if (!entry.isDirectory()) {
    				// do not copy anything from the package cache:
    				if (entry.getName().indexOf("package cache") == -1) {
    					String path = targetPath + entry.getName();
    					File file = new File(path);
    					if (!file.getParentFile().exists()) {
    						file.getParentFile().mkdirs();
    					}
    					FileOutputStream out = new FileOutputStream(file);
    					InputStream in = input.getInputStream(entry);
    					int read;
    					while ((read = in.read(buffer)) != -1) {
    						out.write(buffer, 0, read);
    					}
    					in.close();
    					out.close();
    				}
    			}
    		}
    
    	}
    
    	/**
    	 * Extracts the given resource from a jar-file to the specified directory.
    	 * 
    	 * @param jarFile
    	 *            The jar file which should be unpacked
    	 * @param resource
    	 *            The name of a resource in the jar
    	 * @param targetDir
    	 *            The directory to which the jar-content should be extracted.
    	 * @throws FileNotFoundException
    	 *             when the jarFile does not exist
    	 * @throws IOException
    	 *             when a file could not be written or the jar-file could not read.
    	 */
    	public static void unjar(File jarFile, String resource, File targetDir) throws FileNotFoundException, IOException {
    		// clear target directory:
    		if (targetDir.exists()) {
    			targetDir.delete();
    		}
    		// create new target directory:
    		targetDir.mkdirs();
    		// read jar-file:
    		String targetPath = targetDir.getAbsolutePath() + File.separatorChar;
    		byte[] buffer = new byte[1024 * 1024];
    		JarFile input = new JarFile(jarFile, false, ZipFile.OPEN_READ);
    		Enumeration enumeration = input.entries();
    		for (; enumeration.hasMoreElements();) {
    			JarEntry entry = enumeration.nextElement();
    			if (!entry.isDirectory()) {
    				// do not copy anything from the package cache:
    				if (entry.getName().equals(resource)) {
    					String path = targetPath + entry.getName();
    					File file = new File(path);
    					if (!file.getParentFile().exists()) {
    						file.getParentFile().mkdirs();
    					}
    					FileOutputStream out = new FileOutputStream(file);
    					InputStream in = input.getInputStream(entry);
    					int read;
    					while ((read = in.read(buffer)) != -1) {
    						out.write(buffer, 0, read);
    					}
    					in.close();
    					out.close();
    				}
    			}
    		}
    	}
    
    	/**
    	 * Reads the package-names from the given jar-file.
    	 * 
    	 * @param jarFile
    	 *            the jar file
    	 * @return an array with all found package-names
    	 * @throws IOException
    	 *             when the jar-file could not be read
    	 */
    	public static String[] getPackageNames(File jarFile) throws IOException {
    		HashMap packageNames = new HashMap();
    		JarFile input = new JarFile(jarFile, false, ZipFile.OPEN_READ);
    		Enumeration enumeration = input.entries();
    		for (; enumeration.hasMoreElements();) {
    			JarEntry entry = enumeration.nextElement();
    			String name = entry.getName();
    			if (name.endsWith(".class")) {
    				int endPos = name.lastIndexOf('/');
    				boolean isWindows = false;
    				if (endPos == -1) {
    					endPos = name.lastIndexOf('\\');
    					isWindows = true;
    				}
    				name = name.substring(0, endPos);
    				name = name.replace('/', '.');
    				if (isWindows) {
    					name = name.replace('\\', '.');
    				}
    				packageNames.put(name, name);
    			}
    		}
    		return (String[]) packageNames.values().toArray(new String[packageNames.size()]);
    	}
    
    }
    

    The content of the properties file : jarSignProperties.properties is as follows –
    jarSignProperties.properties

    UNSIGNED_WAR_PATH=C:/exp/fileName.war
    DESTINATION_DIRECTORY_PATH=C:/exp/temp
    SIGNED_WAR=C:/exp/fileName.war
    KEYSTORE_PATH=C:/exp/server.p12
    ALIAS=
    STORE_TYPE=pkcs12
    STORE_PASSWORD=
    

    To compile this code, a dependency to the following needs to be included into your java project (contents of .classpath file provided below) –

    <?xml version="1.0" encoding="UTF-8"?>
    <classpath>
    	<classpathentry kind="src" path="src"/>
    	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/jre6"/>
    	<classpathentry kind="lib" path="lib/commons-lang-2.1.jar"/>
    	<classpathentry kind="lib" path="lib/commons-io-1.1.jar"/>
    	<classpathentry kind="lib" path="C:/Program Files (x86)/Java/jdk1.6.0_25/lib/tools.jar"/>
    	<classpathentry kind="output" path="bin"/>
    </classpath>
    

    Leave a Reply

    Fill in your details below or click an icon to log in:

    WordPress.com Logo

    You are commenting using your WordPress.com account. Log Out /  Change )

    Google photo

    You are commenting using your Google account. Log Out /  Change )

    Twitter picture

    You are commenting using your Twitter account. Log Out /  Change )

    Facebook photo

    You are commenting using your Facebook account. Log Out /  Change )

    Connecting to %s