pt.iflow.api.utils.JarSigner15.java Source code

Java tutorial

Introduction

Here is the source code for pt.iflow.api.utils.JarSigner15.java

Source

/*
 * Copyright 2004 - 2007 University of Cardiff.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package pt.iflow.api.utils;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.KeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipFile;

import org.bouncycastle.util.encoders.Base64;

/**
 * works for Java 1.5.
 * 
 * @author Andrew Harrison
 * @version $Revision: 131 $
 * @created Mar 15, 2007: 2:19:53 PM
 * @date $Date: 2007-04-06 15:31:20 +0100 (Fri, 06 Apr 2007) $ modified by $Author: scmabh $
 * @todo Put your notes here...
 */

public class JarSigner15 {

    private static final String MANIFEST_DIGESTER_CLASS_NAME = "sun.security.util.ManifestDigester";
    private static final Class<?> MANIFEST_DIGESTER_CLASS;

    static {
        Class<?> c = null;
        try {
            c = Class.forName(MANIFEST_DIGESTER_CLASS_NAME);
        } catch (ClassNotFoundException e) {
        }
        MANIFEST_DIGESTER_CLASS = c;
    }

    // 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 JarSigner15(String alias, PrivateKey privateKey, X509Certificate[] certChain) {
        this.alias = alias;
        this.privateKey = privateKey;
        this.certChain = certChain;
    }

    // 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<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                je = 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<String, Attributes> pruneManifest(Manifest manifest, JarFile jarFile) throws IOException {
        Map<String, Attributes> map = manifest.getEntries();
        Iterator<String> 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<String, Attributes> createEntries(Manifest manifest, JarFile jarFile) throws IOException {
        Map<String, Attributes> 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;
    }

    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 new String(Base64.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<String, Attributes> updateManifestDigest(Manifest manifest, JarFile jarFile,
            MessageDigest messageDigest, Map<String, Attributes> entries) throws IOException {
        Enumeration<JarEntry> 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;
    }

    // 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, IllegalAccessException, NoSuchMethodException, InvocationTargetException,
            InstantiationException, ClassNotFoundException {
        // construct the signature file and the signature block for
        // this manifest
        byte[] manifestBytess = serialiseManifest(manifest);
        Object manifestDigester = MANIFEST_DIGESTER_CLASS.getConstructor(byte[].class).newInstance(manifestBytess);
        return new SignatureFile(new MessageDigest[] { messageDigest }, manifest, manifestDigester, this.alias,
                true);

    }

    // 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
     * 
     * @param jarFile Jar file to sign
     * @param outputStream signed jar
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @throws SignatureException
     * @throws IOException
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     * @throws NoSuchMethodException
     * @throws CertificateException
     * @throws InstantiationException
     * @throws ClassNotFoundException
     */
    public void signJarFile(JarFile jarFile, OutputStream outputStream) throws NoSuchAlgorithmException,
            InvalidKeyException, SignatureException, IOException, IllegalAccessException, InvocationTargetException,
            NoSuchMethodException, CertificateException, InstantiationException, ClassNotFoundException {
        /**
         * 
         */

        // 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<String, Attributes> 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, jarFile);

        // 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);
        byte manifestBytes[] = serialiseManifest(manifest);
        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<JarEntry> metaEntries = jarFile.entries();
        while (metaEntries.hasMoreElements()) {
            JarEntry metaEntry = 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<JarEntry> allEntries = jarFile.entries();
        while (allEntries.hasMoreElements()) {
            JarEntry entry = 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();

    }

    private static Constructor<?> findConstructor(Class<?> c, Class<?>... argTypes) throws NoSuchMethodException {
        Constructor<?> ct = c.getDeclaredConstructor(argTypes);
        if (ct == null) {
            throw new RuntimeException(c.getName());
        }
        ct.setAccessible(true);
        return ct;
    }

    private static Method findMethod(Class<?> c, String methodName, Class<?>... argTypes)
            throws NoSuchMethodException {
        Method m = c.getDeclaredMethod(methodName, argTypes);
        if (m == null) {
            throw new RuntimeException(c.getName());
        }
        m.setAccessible(true);
        return m;
    }

    private class SignatureFile {

        private Object sigFile;

        private Class<?> JDKsfClass;

        private Method getMetaNameMethod;
        private Method writeMethod;

        private static final String JDK_SIGNATURE_FILE = "sun.security.tools.SignatureFile";
        private static final String GETMETANAME_METHOD = "getMetaName";
        private static final String WRITE_METHOD = "write";

        public SignatureFile(MessageDigest digests[], Manifest mf, Object md, String baseName, boolean signManifest)
                throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
                IllegalAccessException, InvocationTargetException {

            JDKsfClass = Class.forName(JDK_SIGNATURE_FILE);

            Constructor<?> constructor = findConstructor(JDKsfClass, MessageDigest[].class, Manifest.class,
                    MANIFEST_DIGESTER_CLASS, String.class, Boolean.TYPE);

            sigFile = constructor.newInstance(digests, mf, md, baseName, signManifest);

            getMetaNameMethod = findMethod(JDKsfClass, GETMETANAME_METHOD);
            writeMethod = findMethod(JDKsfClass, WRITE_METHOD, OutputStream.class);
        }

        public Block generateBlock(PrivateKey privateKey, X509Certificate[] certChain, boolean externalSF,
                ZipFile zipFile) throws NoSuchAlgorithmException, InvalidKeyException, IOException,
                SignatureException, CertificateException, ClassNotFoundException, NoSuchMethodException,
                InstantiationException, IllegalAccessException, InvocationTargetException {
            return new Block(this, privateKey, certChain, externalSF, zipFile);
        }

        public Class<?> getJDKSignatureFileClass() {
            return JDKsfClass;
        }

        public Object getJDKSignatureFile() {
            return sigFile;
        }

        public String getMetaName() throws IllegalAccessException, InvocationTargetException {
            return (String) getMetaNameMethod.invoke(sigFile);
        }

        public void write(OutputStream os) throws IllegalAccessException, InvocationTargetException {
            writeMethod.invoke(sigFile, os);
        }

        private class Block {

            private Object block;

            private static final String JDK_BLOCK = JDK_SIGNATURE_FILE + "$Block";
            private static final String JDK_CONTENT_SIGNER = "com.sun.jarsigner.ContentSigner";

            private Method getMetaNameMethod;
            private Method writeMethod;

            public Block(SignatureFile sfg, PrivateKey privateKey, X509Certificate[] certChain, boolean externalSF,
                    ZipFile zipFile) throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
                    IllegalAccessException, InvocationTargetException {

                Class<?> blockClass = Class.forName(JDK_BLOCK);

                Class<?> contentSignerClass = Class.forName(JDK_CONTENT_SIGNER);

                Constructor<?> constructor = findConstructor(blockClass, sfg.getJDKSignatureFileClass(),
                        PrivateKey.class, X509Certificate[].class, Boolean.TYPE, String.class,
                        X509Certificate.class, contentSignerClass, String[].class, ZipFile.class);

                getMetaNameMethod = findMethod(blockClass, GETMETANAME_METHOD);
                writeMethod = findMethod(blockClass, WRITE_METHOD, OutputStream.class);

                block = constructor.newInstance(
                        sfg.getJDKSignatureFile(), /* explicit argument on the constructor */
                        privateKey, certChain, externalSF, null, null, null, null, zipFile);
            }

            public String getMetaName() throws IllegalAccessException, InvocationTargetException {
                return (String) getMetaNameMethod.invoke(block);
            }

            public void write(OutputStream os) throws IllegalAccessException, InvocationTargetException {
                writeMethod.invoke(block, os);
            }
        }
    }

    public static File sign(File jar, String newName, String keystoreLocation, String alias, char[] passwd,
            char[] keypasswd) {

        try {
            FileInputStream fileIn = new FileInputStream(keystoreLocation);
            KeyStore keyStore = KeyStore.getInstance("JKS");
            keyStore.load(fileIn, passwd);
            java.security.cert.Certificate[] chain = keyStore.getCertificateChain(alias);
            X509Certificate certChain[] = new X509Certificate[chain.length];

            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            for (int count = 0; count < chain.length; count++) {
                ByteArrayInputStream certIn = new ByteArrayInputStream(chain[0].getEncoded());
                X509Certificate cert = (X509Certificate) cf.generateCertificate(certIn);
                certChain[count] = cert;
            }

            Key key = keyStore.getKey(alias, keypasswd);
            KeyFactory keyFactory = KeyFactory.getInstance(key.getAlgorithm());
            KeySpec keySpec = keyFactory.getKeySpec(key, RSAPrivateKeySpec.class);
            PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
            JarSigner15 jarSigner = new JarSigner15(alias, privateKey, certChain);

            JarFile jarFile = new JarFile(jar.getCanonicalPath());
            File newJar = new File(jar.getParentFile(), newName);
            OutputStream outStream = new FileOutputStream(newJar);
            jarSigner.signJarFile(jarFile, outStream);
            fileIn.close();
            return newJar;
        } catch (Throwable ex) {
            ex.printStackTrace();
        }
        return null;
    }

    public static File sign(File jar, String keystoreLocation, String alias, char[] passwd, char[] keypasswd) {
        return sign(jar, "s-" + jar.getName(), keystoreLocation, alias, passwd, keypasswd);

    }

}