JavaBuilder/src/dorkbox/build/util/jar/JarSignatureUtil.java

320 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.MessageDigest;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Map;
import java.util.Map.Entry;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.cms.ContentInfo;
import org.bouncycastle.asn1.cms.SignedData;
import dorkbox.util.Base64Fast;
import dorkbox.util.OS;
import dorkbox.util.Sys;
public class JarSignatureUtil {
/**
* a small helper function that will convert a manifest into an array of
* bytes
*/
public static final byte[] serialiseManifest(Manifest manifest) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
manifest.write(baos);
baos.flush();
baos.close();
return baos.toByteArray();
}
/**
* 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)
*/
public static final Map<String, Attributes> updateManifestHashes(Manifest manifest, JarFile jarFile, MessageDigest messageDigest) throws IOException {
Map<String, Attributes> entries = manifest.getEntries();
Enumeration<JarEntry> jarElements = jarFile.entries();
String digestName = messageDigest.getAlgorithm() + "-Digest";
while (jarElements.hasMoreElements()) {
JarEntry jarEntry = jarElements.nextElement();
String name = jarEntry.getName();
if (name.startsWith(JarUtil.metaInfName)) {
continue;
} 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("Name", name); NOT NECESSARY!
InputStream inputStream = jarFile.getInputStream(jarEntry);
attributes.putValue(digestName, JarUtil.updateDigest(inputStream, messageDigest));
Sys.close(inputStream);
entries.put(name, attributes);
}
}
return entries;
}
/**
* @return null if there is a problem with the certificate loading process.
*/
public static final String extractSignatureHashFromSignatureBlock(byte[] signatureBlock) {
ASN1InputStream sigStream = null;
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
InputStream signatureIn = new ByteArrayInputStream(signatureBlock);
sigStream = new ASN1InputStream(signatureIn);
ASN1Primitive signatureASN = sigStream.readObject();
ASN1Sequence seq = ASN1Sequence.getInstance(signatureASN);
ASN1TaggedObject tagged = (ASN1TaggedObject) seq.getObjectAt(1);
// Extract certificates
SignedData newSignedData = SignedData.getInstance(tagged.getObject());
@SuppressWarnings("rawtypes")
Enumeration newSigOjects = newSignedData.getCertificates().getObjects();
Object newSigElement = newSigOjects.nextElement();
if (newSigElement instanceof DERSequence) {
DERSequence newSigDERElement = (DERSequence) newSigElement;
InputStream newSigIn = new ByteArrayInputStream(newSigDERElement.getEncoded());
Certificate newSigCertificate = certFactory.generateCertificate(newSigIn);
// certificate bytes
byte[] newSigCertificateBytes = newSigCertificate.getEncoded();
String encodeToString = Base64Fast.encodeToString(newSigCertificateBytes, false);
return encodeToString;
}
} catch (IOException e) {} catch (CertificateException e) {}
finally {
Sys.close(sigStream);
}
return null;
}
/**
* Verify that the two certificates MATCH from within a signature block (ie,
* XXXXX.DSA in the META-INF directory).
*
* @return true if the two certificates are the same. false otherwise.
*/
public static final boolean compareCertificates(byte[] newSignatureContainerBytes, byte[] oldSignatureContainerBytes) {
ASN1InputStream newSigStream = null;
ASN1InputStream oldSigStream = null;
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
InputStream newSignatureIn = new ByteArrayInputStream(newSignatureContainerBytes);
newSigStream = new ASN1InputStream(newSignatureIn);
ASN1Primitive newSigASNPrim = newSigStream.readObject();
ContentInfo newSigContent = ContentInfo.getInstance(newSigASNPrim);
InputStream oldSignatureIn = new ByteArrayInputStream(oldSignatureContainerBytes);
oldSigStream = new ASN1InputStream(oldSignatureIn);
ASN1Primitive oldSigASNPrim = oldSigStream.readObject();
ContentInfo oldSigContent = ContentInfo.getInstance(oldSigASNPrim);
// Extract certificates
SignedData newSignedData = SignedData.getInstance(newSigContent.getContent());
@SuppressWarnings("rawtypes")
Enumeration newSigOjects = newSignedData.getCertificates().getObjects();
SignedData oldSignedData = SignedData.getInstance(oldSigContent.getContent());
@SuppressWarnings("rawtypes")
Enumeration oldSigOjects = oldSignedData.getCertificates().getObjects();
Object newSigElement = newSigOjects.nextElement();
Object oldSigElement = oldSigOjects.nextElement();
if (newSigElement instanceof DERSequence && oldSigElement instanceof DERSequence) {
DERSequence newSigDERElement = (DERSequence) newSigElement;
InputStream newSigIn = new ByteArrayInputStream(newSigDERElement.getEncoded());
Certificate newSigCertificate = certFactory.generateCertificate(newSigIn);
DERSequence oldSigDERElement = (DERSequence) oldSigElement;
InputStream oldSigIn = new ByteArrayInputStream(oldSigDERElement.getEncoded());
Certificate oldSigCertificate = certFactory.generateCertificate(oldSigIn);
// certificate bytes
byte[] newSigCertificateBytes = newSigCertificate.getEncoded();
byte[] oldSigCertificateBytes = oldSigCertificate.getEncoded();
return Arrays.equals(newSigCertificateBytes, oldSigCertificateBytes);
}
} catch (IOException e) {} catch (CertificateException e) {}
finally {
Sys.close(newSigStream);
Sys.close(oldSigStream);
}
return false;
}
/**
* Creates a NEW signature file manifest based on the supplied message
* digest and manifest.
*/
@SuppressWarnings("deprecation")
public static final Manifest createSignatureFileManifest(MessageDigest messageDigest, Manifest manifest, byte[] manifestBytes) throws IOException, SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
String messageDigestTitle = messageDigest.getAlgorithm() + "-Digest";
// create the new manifest signature (.SF)
Manifest signatureManifest = new Manifest();
Attributes signatureMainAttributes = signatureManifest.getMainAttributes();
signatureMainAttributes.putValue(Attributes.Name.SIGNATURE_VERSION.toString(), "1.0");
String version = System.getProperty("java.version");
String javaVendor = System.getProperty("java.vendor");
signatureMainAttributes.putValue("Created-By", version + " (" + javaVendor + ")");
// SIGN THE WHOLE MANIFEST
messageDigest.reset();
messageDigest.update(manifestBytes, 0, manifestBytes.length);
/*
* Do not insert a default newline at the end of the output line, as
* java.util.jar does its own line management (see
* Manifest.make72Safe()). Inserting additional new lines will cause
* line-wrapping problems.
*/
String entireManifestHash = Base64Fast.encodeToString(messageDigest.digest(), false);
signatureMainAttributes.putValue(messageDigestTitle + "-Manifest", entireManifestHash);
// System.err.println("ENCODED ALL : " + entireManifestHash);
// //////////////////////////
// Instead of reverse engineering the BYTES, we'll just read the
// manifest again and encode on the fly.
// //////////////////////////
ByteArrayOutputStream manifestStream = new ByteArrayOutputStream();
Method writeMainMethod = Attributes.class.getDeclaredMethod("writeMain",
new Class<?>[] {DataOutputStream.class});
writeMainMethod.setAccessible(true);
// MAIN ATTRIBUTES
DataOutputStream dataOutputStream = new DataOutputStream(manifestStream);
// Write out the main attributes for the manifest
writeMainMethod.invoke(manifest.getMainAttributes(), dataOutputStream);
dataOutputStream.flush();
manifestStream.flush();
// HASH the contents of the main attributes (WHICH ARE ALWAYS FIRST!)
byte[] mainAttributesByteArray = manifestStream.toByteArray();
messageDigest.reset();
messageDigest.update(mainAttributesByteArray, 0, mainAttributesByteArray.length);
/*
* Do not insert a default newline at the end of the output line, as
* java.util.jar does its own line management (see
* Manifest.make72Safe()). Inserting additional new lines will cause
* line-wrapping problems.
*/
String mainAttribsManifestHash = Base64Fast.encodeToString(messageDigest.digest(), false);
if (mainAttribsManifestHash != null) {
signatureMainAttributes.putValue(messageDigestTitle + "-Manifest-Main-Attributes", mainAttribsManifestHash);
// System.err.println("ENCODED main: " + mainAttribsManifestHash);
} else {
throw new RuntimeException("Unable to create manifest-main-attribute signature");
}
// PER-ENTRY ATTRIBUTES
Method writeMethod = Attributes.class.getDeclaredMethod("write", new Class<?>[] {DataOutputStream.class});
writeMethod.setAccessible(true);
Method make72Method = Manifest.class.getDeclaredMethod("make72Safe", new Class<?>[] {StringBuffer.class});
make72Method.setAccessible(true);
Map<String, Attributes> entries = manifest.getEntries();
Map<String, Attributes> signatureEntries = signatureManifest.getEntries();
for (Entry<String, Attributes> e : entries.entrySet()) {
manifestStream.reset();
dataOutputStream = new DataOutputStream(manifestStream);
// has to be string buffer.
StringBuffer buffer = new StringBuffer("Name: ");
String entryName = e.getKey();
if (entryName != null) {
byte[] vb = entryName.getBytes(OS.UTF_8); // by doing this, the following new string
// will be safe (UTF-8) despite warnings
entryName = new String(vb, 0, 0, vb.length);
}
buffer.append(entryName);
buffer.append("\r\n"); // must be this because of stupid windows...
make72Method.invoke(null, buffer);
dataOutputStream.writeBytes(buffer.toString());
// Write out the attributes for the manifest
writeMethod.invoke(e.getValue(), dataOutputStream);
dataOutputStream.flush();
manifestStream.flush();
// HASH the contents of the attributes
byte[] attributesByteArray = manifestStream.toByteArray();
messageDigest.reset();
messageDigest.update(attributesByteArray, 0, attributesByteArray.length);
/*
* Do not insert a default newline at the end of the output line, as
* java.util.jar does its own line management (see
* Manifest.make72Safe()). Inserting additional new lines will cause
* line-wrapping problems.
*/
String entryHash = Base64Fast.encodeToString(messageDigest.digest(), false);
Attributes attribute = new Attributes();
attribute.putValue(messageDigestTitle, entryHash);
signatureEntries.put(entryName, attribute);
// System.err.println("ENCODED " + entryName + " : " + entryHash);
}
return signatureManifest;
}
}