Skip to content

Instantly share code, notes, and snippets.

@mmm444
Created October 21, 2013 16:42
Show Gist options
  • Save mmm444/7086899 to your computer and use it in GitHub Desktop.
Save mmm444/7086899 to your computer and use it in GitHub Desktop.
dependencies: org.bouncycastle:bcpkix-jdk15on:1.49, com.google.guava:guava:15.0
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.annotation.Nonnull;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSTypedData;
import org.bouncycastle.cms.SignerInfoGenerator;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DigestCalculatorProvider;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.Store;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;
import com.google.common.io.ByteStreams;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Generator of signed Jars. It stores some data in memory therefore it is not suited for creation of large files. The
* usage:
* <pre>
* KeyStore keystore = KeyStore.getInstance("JKS");
* keyStore.load(keystoreStream, "keystorePassword");
* SimpleSignedJar jar = new SimpleSignedJar(out, keyStore, "keyAlias", "keyPassword");
* signedJar.addManifestAttribute("Main-Class", "com.example.MainClass");
* signedJar.addManifestAttribute("Application-Name", "Example");
* signedJar.addManifestAttribute("Permissions", "all-permissions");
* signedJar.addManifestAttribute("Codebase", "*");
* signedJar.addFileContents("com/example/MainClass.class", clsData);
* signedJar.addFileContents("JNLP-INF/APPLICATION.JNLP", generateJnlpContents());
* signedJar.close();
* </pre>
*
* TODO: add streaming interface for file contents
* TODO: better error handling in #close() method
*
* @author Michal Rydlo
* @see <a href="http://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html#Signed_JAR_File">JAR format
* specification</a>
*/
public class SimpleSignedJar {
private static final int MANIFEST_ATTR_MAX_LEN = 70;
private static final String CREATED_BY = "Oracle Haters Club of inSophy";
private static final String MANIFEST_FN = "META-INF/MANIFEST.MF";
private static final String SIG_FN = "META-INF/SIGNUMO.SF";
private static final String SIG_RSA_FN = "META-INF/SIGNUMO.RSA";
private final ZipOutputStream zos;
private final KeyStore keyStore;
private final String keyAlias;
private final String password;
private final Map<String, String> manifestAttributes;
private final HashFunction hashFunction;
private final String hashFunctionName;
private String manifestHash;
private String manifestMainHash;
private final Map<String, String> fileDigests;
private final Map<String, String> sectionDigests;
/**
* Constructor.
*
* @param out the output stream to write JAR data to
* @param keyStore the key store to load given key from
* @param keyAlias the name of the key in the store, this key is used to sign the JAR
* @param keyPassword the password to access the key
*/
public SimpleSignedJar(@Nonnull OutputStream out, @Nonnull KeyStore keyStore, @Nonnull String keyAlias, @Nonnull String keyPassword) {
this.zos = new ZipOutputStream(checkNotNull(out, "out"));
this.keyStore = checkNotNull(keyStore, "keyStore");
this.keyAlias = checkNotNull(keyAlias, "keyAlias");
this.password = checkNotNull(keyPassword, "keyPassword");
this.manifestAttributes = Maps.newLinkedHashMap();
this.fileDigests = Maps.newLinkedHashMap();
this.sectionDigests = Maps.newLinkedHashMap();
this.hashFunction = Hashing.sha256();
this.hashFunctionName = "SHA-256";
}
/**
* Adds a header to the manifest of the JAR.
*
* @param name name of the attribute, it is placed into the main section of the manifest file, it cannot be longer
* than {@value #MANIFEST_ATTR_MAX_LEN} bytes (in utf-8 encoding)
* @param value value of the attribute
*/
public void addManifestAttribute(@Nonnull String name, @Nonnull String value) {
checkNotNull(name, "name");
checkArgument(name.getBytes(Charsets.UTF_8).length <= MANIFEST_ATTR_MAX_LEN, "attribute name too long");
checkNotNull(value, "value");
manifestAttributes.put(name, value);
}
/**
* Adds a file to the JAR. The file is immediately added to the zipped output stream. This method cannot be called once
* the stream is closed.
*
* @param filename name of the file to add (use forward slash as a path separator)
* @param contents contents of the file
* @throws java.io.IOException
* @throws NullPointerException if any of the arguments is {@code null}
*/
public void addFileContents(@Nonnull String filename, @Nonnull byte[] contents) throws IOException {
checkNotNull(filename, "filename");
checkNotNull(contents, "contents");
zos.putNextEntry(new ZipEntry(filename));
zos.write(contents);
zos.closeEntry();
HashCode hashCode = hashFunction.hashBytes(contents);
String hashCode64 = BaseEncoding.base64().encode(hashCode.asBytes());
fileDigests.put(filename, hashCode64);
}
/**
* Finishes the JAR file by writing the manifest and signature data to it and finishing the ZIP entries. It leaves the
* underlying stream open.
*
* @throws java.io.IOException
* @throws RuntimeException if the signing goes wrong
*/
public void finish() throws IOException {
writeManifest();
byte sig[] = writeSigFile();
writeSignature(sig);
zos.finish();
}
/**
* Closes the JAR file by writing the manifest and signature data to it and finishing the ZIP entries. It closes the
* underlying stream.
*
* @throws java.io.IOException
* @throws RuntimeException if the signing goes wrong
*/
public void close() throws IOException {
finish();
zos.close();
}
/** Creates the beast that can actually sign the data. */
private CMSSignedDataGenerator createSignedDataGenerator() throws Exception {
Security.addProvider(new BouncyCastleProvider());
List<Certificate> certChain = Lists.newArrayList(keyStore.getCertificateChain(keyAlias));
Store certStore = new JcaCertStore(certChain);
Certificate cert = keyStore.getCertificate(keyAlias);
PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, password.toCharArray());
ContentSigner signer = new JcaContentSignerBuilder("SHA256WITHRSA").setProvider("BC").build(privateKey);
CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
DigestCalculatorProvider dcp = new JcaDigestCalculatorProviderBuilder().setProvider("BC").build();
SignerInfoGenerator sig = new JcaSignerInfoGeneratorBuilder(dcp).build(signer, (X509Certificate) cert);
generator.addSignerInfoGenerator(sig);
generator.addCertificates(certStore);
return generator;
}
/** Returns the CMS signed data. */
private byte[] signSigFile(byte[] sigContents) throws Exception {
CMSSignedDataGenerator gen = createSignedDataGenerator();
CMSTypedData cmsData = new CMSProcessableByteArray(sigContents);
CMSSignedData signedData = gen.generate(cmsData, true);
return signedData.getEncoded();
}
/**
* Signs the .SIG file and writes the signature (.RSA file) to the JAR.
*
* @throws java.io.IOException
* @throws RuntimeException if the signing failed
*/
private void writeSignature(byte[] sigFile) throws IOException {
zos.putNextEntry(new ZipEntry(SIG_RSA_FN));
try {
byte[] signature = signSigFile(sigFile);
zos.write(signature);
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("Signing failed.", e);
}
zos.closeEntry();
}
/**
* Writes the .SIG file to the JAR.
*
* @return the contents of the file as bytes
*/
private byte[] writeSigFile() throws IOException {
zos.putNextEntry(new ZipEntry(SIG_FN));
Manifest man = new Manifest();
// main section
Attributes mainAttributes = man.getMainAttributes();
mainAttributes.put(Attributes.Name.SIGNATURE_VERSION, "1.0");
mainAttributes.put(new Attributes.Name("Created-By"), CREATED_BY);
mainAttributes.put(new Attributes.Name(hashFunctionName + "-Digest-Manifest"), manifestHash);
mainAttributes.put(new Attributes.Name(hashFunctionName + "-Digest-Manifest-Main-Attributes"), manifestMainHash);
// individual files sections
Attributes.Name digestAttr = new Attributes.Name(hashFunctionName + "-Digest");
for (Map.Entry<String, String> entry : sectionDigests.entrySet()) {
Attributes attributes = new Attributes();
man.getEntries().put(entry.getKey(), attributes);
attributes.put(digestAttr, entry.getValue());
}
man.write(zos);
zos.closeEntry();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
man.write(baos);
return baos.toByteArray();
}
/** Helper for {@link #writeManifest()} that creates the digest of one entry. */
private String hashEntrySection(String name, Attributes attributes) throws IOException {
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
ByteArrayOutputStream o = new ByteArrayOutputStream();
manifest.write(o);
int emptyLen = o.toByteArray().length;
manifest.getEntries().put(name, attributes);
manifest.write(o);
byte[] ob = o.toByteArray();
ob = Arrays.copyOfRange(ob, emptyLen, ob.length);
return BaseEncoding.base64().encode(hashFunction.hashBytes(ob).asBytes());
}
/** Helper for {@link #writeManifest()} that creates the digest of the main section. */
private String hashMainSection(Attributes attributes) throws IOException {
Manifest manifest = new Manifest();
manifest.getMainAttributes().putAll(attributes);
Hasher hasher = hashFunction.newHasher();
SimpleSignedJar.HashingOutputStream o = new SimpleSignedJar.HashingOutputStream(ByteStreams.nullOutputStream(), hasher);
manifest.write(o);
return BaseEncoding.base64().encode(hasher.hash().asBytes());
}
/**
* Writes the manifest to the JAR. It also calculates the digests that are required to be placed in the the signature
* file.
*
* @throws java.io.IOException
*/
private void writeManifest() throws IOException {
zos.putNextEntry(new ZipEntry(MANIFEST_FN));
Manifest man = new Manifest();
// main section
Attributes mainAttributes = man.getMainAttributes();
mainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
mainAttributes.put(new Attributes.Name("Created-By"), CREATED_BY);
for (Map.Entry<String, String> entry : manifestAttributes.entrySet()) {
mainAttributes.put(new Attributes.Name(entry.getKey()), entry.getValue());
}
// individual files sections
Attributes.Name digestAttr = new Attributes.Name(hashFunctionName + "-Digest");
for (Map.Entry<String, String> entry : fileDigests.entrySet()) {
Attributes attributes = new Attributes();
man.getEntries().put(entry.getKey(), attributes);
attributes.put(digestAttr, entry.getValue());
sectionDigests.put(entry.getKey(), hashEntrySection(entry.getKey(), attributes));
}
Hasher hasher = hashFunction.newHasher();
OutputStream out = new SimpleSignedJar.HashingOutputStream(zos, hasher);
man.write(out);
zos.closeEntry();
manifestHash = BaseEncoding.base64().encode(hasher.hash().asBytes());
manifestMainHash = hashMainSection(man.getMainAttributes());
}
/** Helper output stream that also sends the data to the given {@link com.google.common.hash.Hasher}. */
private static class HashingOutputStream extends OutputStream {
private final OutputStream out;
private final Hasher hasher;
public HashingOutputStream(OutputStream out, Hasher hasher) {
this.out = out;
this.hasher = hasher;
}
@Override
public void write(int b) throws IOException {
out.write(b);
hasher.putByte((byte) b);
}
@Override
public void write(byte[] b) throws IOException {
out.write(b);
hasher.putBytes(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
hasher.putBytes(b, off, len);
}
@Override
public void flush() throws IOException {
out.flush();
}
@Override
public void close() throws IOException {
out.close();
}
}
}
@jredfox
Copy link

jredfox commented Nov 15, 2020

add a verify method. in java programmatically verifying jars is impossible currently unless someone has jdk very bad for the program I am writing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment