dorkbox.build.util.jar.JarSigner.java Source code

Java tutorial

Introduction

Here is the source code for dorkbox.build.util.jar.JarSigner.java

Source

/*
 * Copyright 2012 dorkbox, llc
 *
 * 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 dorkbox.build.util.jar;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.DSAParameter;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.params.DSAKeyParameters;
import org.bouncycastle.crypto.params.DSAParameters;
import org.bouncycastle.crypto.params.DSAPrivateKeyParameters;
import org.bouncycastle.crypto.params.DSAPublicKeyParameters;
import org.bouncycastle.crypto.util.PrivateKeyFactory;
import org.bouncycastle.crypto.util.PublicKeyFactory;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import dorkbox.Build;
import dorkbox.util.Base64Fast;
import dorkbox.util.Sys;
import dorkbox.util.crypto.Crypto;
import dorkbox.util.crypto.CryptoX509;

public class JarSigner {

    static {
        BouncyCastleProvider provider = new BouncyCastleProvider();
        Security.addProvider(provider);
    }

    public static File sign(String jarName, String name) {

        Build.log().println();
        Build.log().title("Signing JAR").println(jarName, name.toUpperCase());

        if (jarName == null) {
            throw new IllegalArgumentException("jarName cannot be null.");
        }

        try {
            File jarFile = new File(jarName);
            ByteArrayOutputStream signJarFile;

            if (jarFile.isFile() && jarFile.canRead()) {
                signJarFile = signJar(jarFile, name);
            } else {
                throw new RuntimeException("Unable to read file: " + jarFile.getCanonicalPath());
            }

            // write out the file
            OutputStream outputStream = new FileOutputStream(jarFile);
            signJarFile.writeTo(outputStream);
            Sys.close(outputStream);

            return new File(jarName);
        } catch (Throwable ex) {
            throw new RuntimeException("Unable to sign jar file! " + ex.getMessage());
        }
    }

    /**
     * the actual JAR signing method
     * @param createDebugVersion
     */
    private static ByteArrayOutputStream signJar(File jarFile, String name) throws IOException,
            IllegalAccessException, InvocationTargetException, NoSuchMethodException, GeneralSecurityException {

        // proper "jar signing" does not allow for ECC signatures to be used. RSA/DSA and that's it.
        // so this "self signed" cert is just that. wimpy.
        // the magic is in the uber-strong ECC key that is used internally, and also has AES keys mixed in.
        DSAKeyParameters[] wimpyKeys = getWimpyKeys();
        DSAPublicKeyParameters wimpyPublicKey = (DSAPublicKeyParameters) wimpyKeys[0];
        DSAPrivateKeyParameters wimpyPrivateKey = (DSAPrivateKeyParameters) wimpyKeys[1];

        // create the certificate
        Calendar expiry = Calendar.getInstance();
        expiry.add(Calendar.YEAR, 2);

        Date startDate = new Date(); // time from which certificate is valid
        Date expiryDate = expiry.getTime(); // time after which certificate is not valid
        BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis()); // serial number for certificate

        X509CertificateHolder wimpyX509CertificateHolder = CryptoX509.DSA.createCertHolder(startDate, expiryDate,
                new X500Name("ST=Lunar Base Alpha, O=Dorkbox, CN=Dorkbox Server, emailaddress=admin@dorkbox.com"),
                new X500Name("ST=Earth, O=Dorkbox, CN=Dorkbox Client, emailaddress=admin@dorkbox.com"),
                serialNumber, wimpyPrivateKey, wimpyPublicKey);

        JarFile jar = new JarFile(jarFile.getCanonicalPath());

        // UNFORTUNATELY, with java6, we CANNOT do anything higher. As such, a CUSTOM signing tool will be developed,
        // which the launcher will verify on it's own.
        // FORTUNATELY, this is will produce the exact same output as if using the command line.
        String digestName = CryptoX509.Util.getDigestNameFromCert(wimpyX509CertificateHolder);
        MessageDigest messageDigest = MessageDigest.getInstance(digestName);

        // get the manifest out of the jar.
        Manifest manifest = JarUtil.getManifestFile(jar);

        // it ONLY exists if it's an "executable" jar
        if (manifest == null) {
            manifest = new Manifest();

            // have to add basic entries.
            Attributes mainAttributes = manifest.getMainAttributes();
            mainAttributes.putValue(Attributes.Name.MANIFEST_VERSION.toString(), "1.0");
        } else {
            // clear out all entries in the manifest
            Map<String, Attributes> entries = manifest.getEntries();
            if (entries.size() > 0) {
                entries.clear();
            }
        }

        // create the message digest and start updating the
        // the attributes in the manifest to contain the SHA digests
        JarSignatureUtil.updateManifestHashes(manifest, jar, messageDigest);

        byte manifestBytes[] = JarSignatureUtil.serialiseManifest(manifest);

        // create a NEW signature file manifest based on the supplied message digest and manifest.
        Manifest signatureFileManifest = JarSignatureUtil.createSignatureFileManifest(messageDigest, manifest,
                manifestBytes);
        byte signatureFileManifestBytes[] = JarSignatureUtil.serialiseManifest(signatureFileManifest);

        byte signatureBlockBytes[] = CryptoX509.createSignature(signatureFileManifestBytes,
                wimpyX509CertificateHolder, wimpyPrivateKey);

        ByteArrayOutputStream byteArrayOutputStream = JarUtil.createNewJar(jar, name, manifestBytes,
                signatureFileManifestBytes, signatureBlockBytes);

        // close the JAR file that we have been using
        jar.close();
        return byteArrayOutputStream;
    }

    public static DSAKeyParameters[] getWimpyKeys() throws IOException, FileNotFoundException {
        String wimpyKeyName = "wimpyCert.key";

        DSAPrivateKeyParameters wimpyPrivateKey = null;
        DSAPublicKeyParameters wimpyPublicKey = null;

        File wimpyKeyRawFile = new File(wimpyKeyName);

        // do we need to create the (wimpy) certificate keys?
        if (!wimpyKeyRawFile.canRead()) {
            // using DSA, since that is compatible with ALL java versions
            @SuppressWarnings("deprecation")
            AsymmetricCipherKeyPair generateKeyPair = Crypto.DSA.generateKeyPair(new SecureRandom(), 8192);
            wimpyPrivateKey = (DSAPrivateKeyParameters) generateKeyPair.getPrivate();
            wimpyPublicKey = (DSAPublicKeyParameters) generateKeyPair.getPublic();

            writeDsaKeysToFile(wimpyPrivateKey, wimpyPublicKey, wimpyKeyRawFile);
        } else {
            FileInputStream inputStream = new FileInputStream(wimpyKeyRawFile);
            long fileSize = inputStream.getChannel().size();

            // check file size.
            if (fileSize > Integer.MAX_VALUE - 1) {
                System.err.println(
                        "Corrupt wimpyKeyFile! " + wimpyKeyRawFile.getAbsolutePath() + " Creating a new one.");

                // using DSA, since that is compatible with ALL java versions
                @SuppressWarnings("deprecation")
                AsymmetricCipherKeyPair generateKeyPair = Crypto.DSA.generateKeyPair(new SecureRandom(), 8192);
                wimpyPrivateKey = (DSAPrivateKeyParameters) generateKeyPair.getPrivate();
                wimpyPublicKey = (DSAPublicKeyParameters) generateKeyPair.getPublic();

                writeDsaKeysToFile(wimpyPrivateKey, wimpyPublicKey, wimpyKeyRawFile);
            } else {
                // read in the entire file as bytes.
                int fileSizeAsInt = (int) fileSize;

                byte[] inputBytes = new byte[fileSizeAsInt];
                inputStream.read(inputBytes, 0, fileSizeAsInt);
                Sys.close(inputStream);

                // read public key length
                int wimpyPublicKeyLength = (inputBytes[fileSizeAsInt - 4] & 0xff) << 24
                        | (inputBytes[fileSizeAsInt - 3] & 0xff) << 16 | (inputBytes[fileSizeAsInt - 2] & 0xff) << 8
                        | (inputBytes[fileSizeAsInt - 1] & 0xff) << 0;

                byte[] publicKeyBytes = new byte[wimpyPublicKeyLength];
                byte[] privateKeyBytes = new byte[fileSizeAsInt - 4 - wimpyPublicKeyLength];

                System.arraycopy(inputBytes, 0, publicKeyBytes, 0, publicKeyBytes.length);
                System.arraycopy(inputBytes, publicKeyBytes.length, privateKeyBytes, 0, privateKeyBytes.length);

                displayByteHash(publicKeyBytes);

                wimpyPublicKey = (DSAPublicKeyParameters) PublicKeyFactory.createKey(publicKeyBytes);
                wimpyPrivateKey = (DSAPrivateKeyParameters) PrivateKeyFactory.createKey(privateKeyBytes);
            }
        }

        return new DSAKeyParameters[] { wimpyPublicKey, wimpyPrivateKey };
    }

    private static void writeDsaKeysToFile(DSAPrivateKeyParameters wimpyPrivateKey,
            DSAPublicKeyParameters wimpyPublicKey, File wimpyKeyRawFile) throws IOException, FileNotFoundException {

        DSAParameters parameters = wimpyPublicKey.getParameters(); // has to convert to DSAParameter so encoding works.
        byte[] publicKeyBytes = new SubjectPublicKeyInfo(new AlgorithmIdentifier(X9ObjectIdentifiers.id_dsa,
                new DSAParameter(parameters.getP(), parameters.getQ(), parameters.getG()).toASN1Primitive()),
                new ASN1Integer(wimpyPublicKey.getY())).getEncoded();
        // SAME AS:
        //        Certificate[] certificates = Launcher.class.getProtectionDomain().getCodeSource().getCertificates();
        //        if (certificates.length != 1) {
        //            // WHOOPS!
        //            Exit.FailedSecurity("Incorrect certificate length!");
        //        }
        //
        //        Certificate certificate = certificates[0];
        //        PublicKey publicKey = certificate.getPublicKey();
        //        byte[] publicKeyBytes = publicKey.getEncoded();
        //
        //        digest.reset();
        //        digest.update(publicKeyBytes, 0, publicKeyBytes.length);
        //        hashPublicKeyBytes = digest.digest();

        parameters = wimpyPrivateKey.getParameters();
        byte[] privateKeyBytes = new PrivateKeyInfo(new AlgorithmIdentifier(X9ObjectIdentifiers.id_dsa,
                new DSAParameter(parameters.getP(), parameters.getQ(), parameters.getG()).toASN1Primitive()),
                new ASN1Integer(wimpyPrivateKey.getX())).getEncoded();

        // write public length to bytes.
        byte[] publicKeySize = new byte[] { (byte) (publicKeyBytes.length >>> 24),
                (byte) (publicKeyBytes.length >>> 16), (byte) (publicKeyBytes.length >>> 8),
                (byte) (publicKeyBytes.length >>> 0) };

        ByteArrayOutputStream keyOutputStream = new ByteArrayOutputStream(
                4 + publicKeyBytes.length + privateKeyBytes.length);

        keyOutputStream.write(publicKeyBytes, 0, publicKeyBytes.length);
        keyOutputStream.write(privateKeyBytes, 0, privateKeyBytes.length);
        keyOutputStream.write(publicKeySize, 0, publicKeySize.length); // mess with people staring at the keys (store length at the end).

        displayByteHash(publicKeyBytes);

        // write out the file
        OutputStream outputStream = new FileOutputStream(wimpyKeyRawFile);
        keyOutputStream.writeTo(outputStream);
        Sys.close(outputStream);
    }

    private static void displayByteHash(byte[] publicKeyBytes) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-512");
            digest.reset();
            digest.update(publicKeyBytes, 0, publicKeyBytes.length);

            String digestString = Base64Fast.encodeToString(digest.digest(), false);

            String origDigestHash = "9f5LkG90ITAMR37xxbXGXAGyaGkZL1dP7FzU8y/CL8gskIxegZTRbOn0g3ks/eCJ5jSKTX4eVZCPmA0TKj7zlw==";
            if (!digestString.equals(origDigestHash)) {
                System.err.println("Wimpy public key bytes. Need to modify " + JarSigner.class.getSimpleName()
                        + " and in the Launcher");
                System.err.println(digestString);
            }

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }
}