316 lines
14 KiB
Java
316 lines
14 KiB
Java
/*
|
|
* 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().message();
|
|
Build.log().title("Signing JAR").message(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();
|
|
}
|
|
}
|
|
} |