320 lines
14 KiB
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;
|
|
}
|
|
|
|
}
|