public
Created

dependencies: org.bouncycastle:bcpkix-jdk15on:1.49, com.google.guava:guava:15.0

  • Download Gist
SimpleSignedJar.java
Java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
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();
}
}
}

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.