blob: adfe9a3d76ac9f5ab83954dc7e38ace1353e1a6d [file] [log] [blame]
The Android Open Source Project88b60792009-03-03 19:28:42 -08001/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.signapk;
18
Doug Zongker147626e2012-09-04 13:32:13 -070019import org.bouncycastle.asn1.ASN1InputStream;
20import org.bouncycastle.asn1.ASN1ObjectIdentifier;
21import org.bouncycastle.asn1.DEROutputStream;
22import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
23import org.bouncycastle.cert.jcajce.JcaCertStore;
24import org.bouncycastle.cms.CMSException;
25import org.bouncycastle.cms.CMSProcessableByteArray;
26import org.bouncycastle.cms.CMSSignedData;
27import org.bouncycastle.cms.CMSSignedDataGenerator;
28import org.bouncycastle.cms.CMSTypedData;
29import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
30import org.bouncycastle.jce.provider.BouncyCastleProvider;
31import org.bouncycastle.operator.ContentSigner;
32import org.bouncycastle.operator.OperatorCreationException;
33import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
34import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
35import org.bouncycastle.util.encoders.Base64;
The Android Open Source Project88b60792009-03-03 19:28:42 -080036
37import java.io.BufferedReader;
Koushik Dutta29706d12012-12-17 22:25:22 -080038import java.io.BufferedOutputStream;
The Android Open Source Project88b60792009-03-03 19:28:42 -080039import java.io.ByteArrayOutputStream;
40import java.io.DataInputStream;
41import java.io.File;
42import java.io.FileInputStream;
43import java.io.FileOutputStream;
44import java.io.FilterOutputStream;
45import java.io.IOException;
46import java.io.InputStream;
47import java.io.InputStreamReader;
48import java.io.OutputStream;
49import java.io.PrintStream;
The Android Open Source Project88b60792009-03-03 19:28:42 -080050import java.security.DigestOutputStream;
51import java.security.GeneralSecurityException;
52import java.security.Key;
53import java.security.KeyFactory;
54import java.security.MessageDigest;
55import java.security.PrivateKey;
Doug Zongker147626e2012-09-04 13:32:13 -070056import java.security.Provider;
57import java.security.Security;
58import java.security.cert.CertificateEncodingException;
The Android Open Source Project88b60792009-03-03 19:28:42 -080059import java.security.cert.CertificateFactory;
60import java.security.cert.X509Certificate;
61import java.security.spec.InvalidKeySpecException;
62import java.security.spec.KeySpec;
63import java.security.spec.PKCS8EncodedKeySpec;
64import java.util.ArrayList;
65import java.util.Collections;
The Android Open Source Project88b60792009-03-03 19:28:42 -080066import java.util.Enumeration;
The Android Open Source Project88b60792009-03-03 19:28:42 -080067import java.util.Map;
68import java.util.TreeMap;
69import java.util.jar.Attributes;
70import java.util.jar.JarEntry;
71import java.util.jar.JarFile;
72import java.util.jar.JarOutputStream;
73import java.util.jar.Manifest;
Doug Zongkeraf482b62009-06-08 10:46:55 -070074import java.util.regex.Pattern;
The Android Open Source Project88b60792009-03-03 19:28:42 -080075import javax.crypto.Cipher;
76import javax.crypto.EncryptedPrivateKeyInfo;
77import javax.crypto.SecretKeyFactory;
78import javax.crypto.spec.PBEKeySpec;
79
80/**
81 * Command line tool to sign JAR files (including APKs and OTA updates) in
82 * a way compatible with the mincrypt verifier, using SHA1 and RSA keys.
83 */
84class SignApk {
85 private static final String CERT_SF_NAME = "META-INF/CERT.SF";
86 private static final String CERT_RSA_NAME = "META-INF/CERT.RSA";
Doug Zongkerb14c9762012-10-15 17:10:13 -070087 private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
88 private static final String CERT_RSA_MULTI_NAME = "META-INF/CERT%d.RSA";
The Android Open Source Project88b60792009-03-03 19:28:42 -080089
Doug Zongker7bb04232012-05-11 09:20:50 -070090 private static final String OTACERT_NAME = "META-INF/com/android/otacert";
91
Doug Zongker147626e2012-09-04 13:32:13 -070092 private static Provider sBouncyCastleProvider;
93
Doug Zongkeraf482b62009-06-08 10:46:55 -070094 // Files matching this pattern are not copied to the output.
95 private static Pattern stripPattern =
Doug Zongkerb14c9762012-10-15 17:10:13 -070096 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA)|com/android/otacert))|(" +
97 Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
Doug Zongkeraf482b62009-06-08 10:46:55 -070098
The Android Open Source Project88b60792009-03-03 19:28:42 -080099 private static X509Certificate readPublicKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800100 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800101 FileInputStream input = new FileInputStream(file);
102 try {
103 CertificateFactory cf = CertificateFactory.getInstance("X.509");
104 return (X509Certificate) cf.generateCertificate(input);
105 } finally {
106 input.close();
107 }
108 }
109
110 /**
111 * Reads the password from stdin and returns it as a string.
112 *
113 * @param keyFile The file containing the private key. Used to prompt the user.
114 */
115 private static String readPassword(File keyFile) {
116 // TODO: use Console.readPassword() when it's available.
117 System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
118 System.out.flush();
119 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
120 try {
121 return stdin.readLine();
122 } catch (IOException ex) {
123 return null;
124 }
125 }
126
127 /**
128 * Decrypt an encrypted PKCS 8 format private key.
129 *
130 * Based on ghstark's post on Aug 6, 2006 at
131 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
132 *
133 * @param encryptedPrivateKey The raw data of the private key
134 * @param keyFile The file containing the private key
135 */
136 private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
Koushik Dutta29706d12012-12-17 22:25:22 -0800137 throws GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800138 EncryptedPrivateKeyInfo epkInfo;
139 try {
140 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
141 } catch (IOException ex) {
142 // Probably not an encrypted key.
143 return null;
144 }
145
146 char[] password = readPassword(keyFile).toCharArray();
147
148 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
149 Key key = skFactory.generateSecret(new PBEKeySpec(password));
150
151 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
152 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
153
154 try {
155 return epkInfo.getKeySpec(cipher);
156 } catch (InvalidKeySpecException ex) {
157 System.err.println("signapk: Password for " + keyFile + " may be bad.");
158 throw ex;
159 }
160 }
161
162 /** Read a PKCS 8 format private key. */
163 private static PrivateKey readPrivateKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800164 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800165 DataInputStream input = new DataInputStream(new FileInputStream(file));
166 try {
167 byte[] bytes = new byte[(int) file.length()];
168 input.read(bytes);
169
170 KeySpec spec = decryptPrivateKey(bytes, file);
171 if (spec == null) {
172 spec = new PKCS8EncodedKeySpec(bytes);
173 }
174
175 try {
176 return KeyFactory.getInstance("RSA").generatePrivate(spec);
177 } catch (InvalidKeySpecException ex) {
178 return KeyFactory.getInstance("DSA").generatePrivate(spec);
179 }
180 } finally {
181 input.close();
182 }
183 }
184
185 /** Add the SHA1 of every file to the manifest, creating it if necessary. */
186 private static Manifest addDigestsToManifest(JarFile jar)
Koushik Dutta29706d12012-12-17 22:25:22 -0800187 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800188 Manifest input = jar.getManifest();
189 Manifest output = new Manifest();
190 Attributes main = output.getMainAttributes();
191 if (input != null) {
192 main.putAll(input.getMainAttributes());
193 } else {
194 main.putValue("Manifest-Version", "1.0");
195 main.putValue("Created-By", "1.0 (Android SignApk)");
196 }
197
The Android Open Source Project88b60792009-03-03 19:28:42 -0800198 MessageDigest md = MessageDigest.getInstance("SHA1");
199 byte[] buffer = new byte[4096];
200 int num;
201
202 // We sort the input entries by name, and add them to the
203 // output manifest in sorted order. We expect that the output
204 // map will be deterministic.
205
206 TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
207
208 for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
209 JarEntry entry = e.nextElement();
210 byName.put(entry.getName(), entry);
211 }
212
213 for (JarEntry entry: byName.values()) {
214 String name = entry.getName();
Doug Zongkerb14c9762012-10-15 17:10:13 -0700215 if (!entry.isDirectory() &&
216 (stripPattern == null || !stripPattern.matcher(name).matches())) {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800217 InputStream data = jar.getInputStream(entry);
218 while ((num = data.read(buffer)) > 0) {
219 md.update(buffer, 0, num);
220 }
221
222 Attributes attr = null;
223 if (input != null) attr = input.getAttributes(name);
224 attr = attr != null ? new Attributes(attr) : new Attributes();
Doug Zongker147626e2012-09-04 13:32:13 -0700225 attr.putValue("SHA1-Digest",
226 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800227 output.getEntries().put(name, attr);
228 }
229 }
230
231 return output;
232 }
233
Doug Zongker7bb04232012-05-11 09:20:50 -0700234 /**
235 * Add a copy of the public key to the archive; this should
236 * exactly match one of the files in
237 * /system/etc/security/otacerts.zip on the device. (The same
238 * cert can be extracted from the CERT.RSA file but this is much
239 * easier to get at.)
240 */
241 private static void addOtacert(JarOutputStream outputJar,
242 File publicKeyFile,
243 long timestamp,
244 Manifest manifest)
245 throws IOException, GeneralSecurityException {
Doug Zongker7bb04232012-05-11 09:20:50 -0700246 MessageDigest md = MessageDigest.getInstance("SHA1");
247
248 JarEntry je = new JarEntry(OTACERT_NAME);
249 je.setTime(timestamp);
250 outputJar.putNextEntry(je);
251 FileInputStream input = new FileInputStream(publicKeyFile);
252 byte[] b = new byte[4096];
253 int read;
254 while ((read = input.read(b)) != -1) {
255 outputJar.write(b, 0, read);
256 md.update(b, 0, read);
257 }
258 input.close();
259
260 Attributes attr = new Attributes();
Doug Zongker147626e2012-09-04 13:32:13 -0700261 attr.putValue("SHA1-Digest",
262 new String(Base64.encode(md.digest()), "ASCII"));
Doug Zongker7bb04232012-05-11 09:20:50 -0700263 manifest.getEntries().put(OTACERT_NAME, attr);
264 }
265
266
Doug Zongker147626e2012-09-04 13:32:13 -0700267 /** Write to another stream and track how many bytes have been
268 * written.
269 */
270 private static class CountOutputStream extends FilterOutputStream {
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700271 private int mCount;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800272
Doug Zongker147626e2012-09-04 13:32:13 -0700273 public CountOutputStream(OutputStream out) {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800274 super(out);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700275 mCount = 0;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800276 }
277
278 @Override
279 public void write(int b) throws IOException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800280 super.write(b);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700281 mCount++;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800282 }
283
284 @Override
285 public void write(byte[] b, int off, int len) throws IOException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800286 super.write(b, off, len);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700287 mCount += len;
288 }
289
290 public int size() {
291 return mCount;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800292 }
293 }
294
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700295 /** Write a .SF file with a digest of the specified manifest. */
Doug Zongker147626e2012-09-04 13:32:13 -0700296 private static void writeSignatureFile(Manifest manifest, OutputStream out)
297 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800298 Manifest sf = new Manifest();
299 Attributes main = sf.getMainAttributes();
300 main.putValue("Signature-Version", "1.0");
301 main.putValue("Created-By", "1.0 (Android SignApk)");
302
The Android Open Source Project88b60792009-03-03 19:28:42 -0800303 MessageDigest md = MessageDigest.getInstance("SHA1");
304 PrintStream print = new PrintStream(
Koushik Dutta29706d12012-12-17 22:25:22 -0800305 new DigestOutputStream(new ByteArrayOutputStream(), md),
306 true, "UTF-8");
The Android Open Source Project88b60792009-03-03 19:28:42 -0800307
308 // Digest of the entire manifest
309 manifest.write(print);
310 print.flush();
Doug Zongker147626e2012-09-04 13:32:13 -0700311 main.putValue("SHA1-Digest-Manifest",
312 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800313
314 Map<String, Attributes> entries = manifest.getEntries();
315 for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
316 // Digest of the manifest stanza for this entry.
317 print.print("Name: " + entry.getKey() + "\r\n");
318 for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
319 print.print(att.getKey() + ": " + att.getValue() + "\r\n");
320 }
321 print.print("\r\n");
322 print.flush();
323
324 Attributes sfAttr = new Attributes();
Doug Zongker147626e2012-09-04 13:32:13 -0700325 sfAttr.putValue("SHA1-Digest",
326 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800327 sf.getEntries().put(entry.getKey(), sfAttr);
328 }
329
Doug Zongker147626e2012-09-04 13:32:13 -0700330 CountOutputStream cout = new CountOutputStream(out);
331 sf.write(cout);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700332
333 // A bug in the java.util.jar implementation of Android platforms
334 // up to version 1.6 will cause a spurious IOException to be thrown
335 // if the length of the signature file is a multiple of 1024 bytes.
336 // As a workaround, add an extra CRLF in this case.
Doug Zongker147626e2012-09-04 13:32:13 -0700337 if ((cout.size() % 1024) == 0) {
338 cout.write('\r');
339 cout.write('\n');
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700340 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800341 }
342
Doug Zongker147626e2012-09-04 13:32:13 -0700343 /** Sign data and write the digital signature to 'out'. */
The Android Open Source Project88b60792009-03-03 19:28:42 -0800344 private static void writeSignatureBlock(
Doug Zongker147626e2012-09-04 13:32:13 -0700345 CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
346 OutputStream out)
347 throws IOException,
348 CertificateEncodingException,
349 OperatorCreationException,
350 CMSException {
351 ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
352 certList.add(publicKey);
353 JcaCertStore certs = new JcaCertStore(certList);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800354
Doug Zongker147626e2012-09-04 13:32:13 -0700355 CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
356 ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA")
357 .setProvider(sBouncyCastleProvider)
358 .build(privateKey);
359 gen.addSignerInfoGenerator(
360 new JcaSignerInfoGeneratorBuilder(
361 new JcaDigestCalculatorProviderBuilder()
362 .setProvider(sBouncyCastleProvider)
363 .build())
364 .setDirectSignature(true)
365 .build(sha1Signer, publicKey));
366 gen.addCertificates(certs);
367 CMSSignedData sigData = gen.generate(data, false);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800368
Doug Zongker147626e2012-09-04 13:32:13 -0700369 ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
370 DEROutputStream dos = new DEROutputStream(out);
371 dos.writeObject(asn1.readObject());
The Android Open Source Project88b60792009-03-03 19:28:42 -0800372 }
373
Koushik Dutta29706d12012-12-17 22:25:22 -0800374 /**
375 * Copy all the files in a manifest from input to output. We set
376 * the modification times in the output to a fixed time, so as to
377 * reduce variation in the output file and make incremental OTAs
378 * more efficient.
379 */
380 private static void copyFiles(Manifest manifest,
381 JarFile in, JarOutputStream out, long timestamp) throws IOException {
382 byte[] buffer = new byte[4096];
383 int num;
384
385 Map<String, Attributes> entries = manifest.getEntries();
386 ArrayList<String> names = new ArrayList<String>(entries.keySet());
387 Collections.sort(names);
388 for (String name : names) {
389 JarEntry inEntry = in.getJarEntry(name);
390 JarEntry outEntry = null;
391 if (inEntry.getMethod() == JarEntry.STORED) {
392 // Preserve the STORED method of the input entry.
393 outEntry = new JarEntry(inEntry);
394 } else {
395 // Create a new entry so that the compressed len is recomputed.
396 outEntry = new JarEntry(name);
397 }
398 outEntry.setTime(timestamp);
399 out.putNextEntry(outEntry);
400
401 InputStream data = in.getInputStream(inEntry);
402 while ((num = data.read(buffer)) > 0) {
403 out.write(buffer, 0, num);
404 }
405 out.flush();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700406 }
Koushik Dutta29706d12012-12-17 22:25:22 -0800407 }
408
409 private static class WholeFileSignerOutputStream extends FilterOutputStream {
410 private boolean closing = false;
411 private ByteArrayOutputStream footer = new ByteArrayOutputStream();
412 private OutputStream tee;
413
414 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
415 super(out);
416 this.tee = tee;
417 }
418
419 public void notifyClosing() {
420 closing = true;
421 }
422
423 public void finish() throws IOException {
424 closing = false;
425
426 byte[] data = footer.toByteArray();
427 if (data.length < 2)
428 throw new IOException("Less than two bytes written to footer");
429 write(data, 0, data.length - 2);
430 }
431
432 public byte[] getTail() {
433 return footer.toByteArray();
434 }
435
436 @Override
437 public void write(byte[] b) throws IOException {
438 write(b, 0, b.length);
439 }
440
441 @Override
442 public void write(byte[] b, int off, int len) throws IOException {
443 if (closing) {
444 // if the jar is about to close, save the footer that will be written
445 footer.write(b, off, len);
446 }
447 else {
448 // write to both output streams. out is the CMSTypedData signer and tee is the file.
449 out.write(b, off, len);
450 tee.write(b, off, len);
451 }
452 }
453
454 @Override
455 public void write(int b) throws IOException {
456 if (closing) {
457 // if the jar is about to close, save the footer that will be written
458 footer.write(b);
459 }
460 else {
461 // write to both output streams. out is the CMSTypedData signer and tee is the file.
462 out.write(b);
463 tee.write(b);
464 }
465 }
466 }
467
468 private static class CMSSigner implements CMSTypedData {
469 private JarFile inputJar;
470 private File publicKeyFile;
471 private X509Certificate publicKey;
472 private PrivateKey privateKey;
473 private String outputFile;
474 private OutputStream outputStream;
475 private final ASN1ObjectIdentifier type;
476 private WholeFileSignerOutputStream signer;
477
478 public CMSSigner(JarFile inputJar, File publicKeyFile,
479 X509Certificate publicKey, PrivateKey privateKey,
480 OutputStream outputStream) {
481 this.inputJar = inputJar;
482 this.publicKeyFile = publicKeyFile;
483 this.publicKey = publicKey;
484 this.privateKey = privateKey;
485 this.outputStream = outputStream;
486 this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
487 }
488
489 public Object getContent() {
490 throw new UnsupportedOperationException();
491 }
492
493 public ASN1ObjectIdentifier getContentType() {
494 return type;
495 }
496
497 public void write(OutputStream out) throws IOException {
498 try {
499 signer = new WholeFileSignerOutputStream(out, outputStream);
500 JarOutputStream outputJar = new JarOutputStream(signer);
501
502 Manifest manifest = addDigestsToManifest(inputJar);
503 signFile(manifest, inputJar,
504 new X509Certificate[]{ publicKey },
505 new PrivateKey[]{ privateKey },
506 outputJar);
507 // Assume the certificate is valid for at least an hour.
508 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
509 addOtacert(outputJar, publicKeyFile, timestamp, manifest);
510
511 signer.notifyClosing();
512 outputJar.close();
513 signer.finish();
514 }
515 catch (Exception e) {
516 throw new IOException(e);
517 }
518 }
519
520 public void writeSignatureBlock(ByteArrayOutputStream temp)
521 throws IOException,
522 CertificateEncodingException,
523 OperatorCreationException,
524 CMSException {
525 SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
526 }
527
528 public WholeFileSignerOutputStream getSigner() {
529 return signer;
530 }
531 }
532
533 private static void signWholeFile(JarFile inputJar, File publicKeyFile,
534 X509Certificate publicKey, PrivateKey privateKey,
535 OutputStream outputStream) throws Exception {
536 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
537 publicKey, privateKey, outputStream);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700538
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700539 ByteArrayOutputStream temp = new ByteArrayOutputStream();
540
541 // put a readable message and a null char at the start of the
542 // archive comment, so that tools that display the comment
543 // (hopefully) show something sensible.
544 // TODO: anything more useful we can put in this message?
545 byte[] message = "signed by SignApk".getBytes("UTF-8");
546 temp.write(message);
547 temp.write(0);
Doug Zongker147626e2012-09-04 13:32:13 -0700548
Koushik Dutta29706d12012-12-17 22:25:22 -0800549 cmsOut.writeSignatureBlock(temp);
550
551 byte[] zipData = cmsOut.getSigner().getTail();
552
553 // For a zip with no archive comment, the
554 // end-of-central-directory record will be 22 bytes long, so
555 // we expect to find the EOCD marker 22 bytes from the end.
556 if (zipData[zipData.length-22] != 0x50 ||
557 zipData[zipData.length-21] != 0x4b ||
558 zipData[zipData.length-20] != 0x05 ||
559 zipData[zipData.length-19] != 0x06) {
560 throw new IllegalArgumentException("zip data already has an archive comment");
561 }
562
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700563 int total_size = temp.size() + 6;
564 if (total_size > 0xffff) {
565 throw new IllegalArgumentException("signature is too big for ZIP file comment");
566 }
567 // signature starts this many bytes from the end of the file
568 int signature_start = total_size - message.length - 1;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700569 temp.write(signature_start & 0xff);
570 temp.write((signature_start >> 8) & 0xff);
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700571 // Why the 0xff bytes? In a zip file with no archive comment,
572 // bytes [-6:-2] of the file are the little-endian offset from
573 // the start of the file to the central directory. So for the
574 // two high bytes to be 0xff 0xff, the archive would have to
Doug Zongker147626e2012-09-04 13:32:13 -0700575 // be nearly 4GB in size. So it's unlikely that a real
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700576 // commentless archive would have 0xffs here, and lets us tell
577 // an old signed archive from a new one.
578 temp.write(0xff);
579 temp.write(0xff);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700580 temp.write(total_size & 0xff);
581 temp.write((total_size >> 8) & 0xff);
582 temp.flush();
583
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700584 // Signature verification checks that the EOCD header is the
585 // last such sequence in the file (to avoid minzip finding a
586 // fake EOCD appended after the signature in its scan). The
587 // odds of producing this sequence by chance are very low, but
588 // let's catch it here if it does.
589 byte[] b = temp.toByteArray();
590 for (int i = 0; i < b.length-3; ++i) {
591 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
592 throw new IllegalArgumentException("found spurious EOCD header at " + i);
593 }
594 }
595
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700596 outputStream.write(total_size & 0xff);
597 outputStream.write((total_size >> 8) & 0xff);
598 temp.writeTo(outputStream);
599 }
600
Koushik Dutta29706d12012-12-17 22:25:22 -0800601 private static void signFile(Manifest manifest, JarFile inputJar,
602 X509Certificate[] publicKey, PrivateKey[] privateKey,
603 JarOutputStream outputJar)
604 throws Exception {
605 // Assume the certificate is valid for at least an hour.
606 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800607
Koushik Dutta29706d12012-12-17 22:25:22 -0800608 JarEntry je;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800609
Koushik Dutta29706d12012-12-17 22:25:22 -0800610 // Everything else
611 copyFiles(manifest, inputJar, outputJar, timestamp);
612
613 // MANIFEST.MF
614 je = new JarEntry(JarFile.MANIFEST_NAME);
615 je.setTime(timestamp);
616 outputJar.putNextEntry(je);
617 manifest.write(outputJar);
618
619 int numKeys = publicKey.length;
620 for (int k = 0; k < numKeys; ++k) {
621 // CERT.SF / CERT#.SF
622 je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
623 (String.format(CERT_SF_MULTI_NAME, k)));
624 je.setTime(timestamp);
625 outputJar.putNextEntry(je);
626 ByteArrayOutputStream baos = new ByteArrayOutputStream();
627 writeSignatureFile(manifest, baos);
628 byte[] signedData = baos.toByteArray();
629 outputJar.write(signedData);
630
631 // CERT.RSA / CERT#.RSA
632 je = new JarEntry(numKeys == 1 ? CERT_RSA_NAME :
633 (String.format(CERT_RSA_MULTI_NAME, k)));
634 je.setTime(timestamp);
635 outputJar.putNextEntry(je);
636 writeSignatureBlock(new CMSProcessableByteArray(signedData),
637 publicKey[k], privateKey[k], outputJar);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800638 }
639 }
640
Doug Zongkerb14c9762012-10-15 17:10:13 -0700641 private static void usage() {
642 System.err.println("Usage: signapk [-w] " +
643 "publickey.x509[.pem] privatekey.pk8 " +
644 "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
645 "input.jar output.jar");
646 System.exit(2);
647 }
648
The Android Open Source Project88b60792009-03-03 19:28:42 -0800649 public static void main(String[] args) {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700650 if (args.length < 4) usage();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800651
Doug Zongker147626e2012-09-04 13:32:13 -0700652 sBouncyCastleProvider = new BouncyCastleProvider();
653 Security.addProvider(sBouncyCastleProvider);
654
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700655 boolean signWholeFile = false;
656 int argstart = 0;
657 if (args[0].equals("-w")) {
658 signWholeFile = true;
659 argstart = 1;
660 }
661
Doug Zongkerb14c9762012-10-15 17:10:13 -0700662 if ((args.length - argstart) % 2 == 1) usage();
663 int numKeys = ((args.length - argstart) / 2) - 1;
664 if (signWholeFile && numKeys > 1) {
665 System.err.println("Only one key may be used with -w.");
666 System.exit(2);
667 }
668
669 String inputFilename = args[args.length-2];
670 String outputFilename = args[args.length-1];
671
The Android Open Source Project88b60792009-03-03 19:28:42 -0800672 JarFile inputJar = null;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700673 FileOutputStream outputFile = null;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800674
675 try {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700676 File firstPublicKeyFile = new File(args[argstart+0]);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800677
Doug Zongkerb14c9762012-10-15 17:10:13 -0700678 X509Certificate[] publicKey = new X509Certificate[numKeys];
679 for (int i = 0; i < numKeys; ++i) {
680 int argNum = argstart + i*2;
681 publicKey[i] = readPublicKey(new File(args[argNum]));
682 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800683
Doug Zongkerb14c9762012-10-15 17:10:13 -0700684 // Set the ZIP file timestamp to the starting valid time
685 // of the 0th certificate plus one hour (to match what
686 // we've historically done).
687 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
688
689 PrivateKey[] privateKey = new PrivateKey[numKeys];
690 for (int i = 0; i < numKeys; ++i) {
691 int argNum = argstart + i*2 + 1;
692 privateKey[i] = readPrivateKey(new File(args[argNum]));
693 }
694 inputJar = new JarFile(new File(inputFilename), false); // Don't verify.
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700695
Koushik Dutta29706d12012-12-17 22:25:22 -0800696 outputFile = new FileOutputStream(outputFilename);
697
698
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700699 if (signWholeFile) {
Koushik Dutta29706d12012-12-17 22:25:22 -0800700 SignApk.signWholeFile(inputJar, firstPublicKeyFile,
701 publicKey[0], privateKey[0], outputFile);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700702 } else {
Koushik Dutta29706d12012-12-17 22:25:22 -0800703 JarOutputStream outputJar = new JarOutputStream(outputFile);
Doug Zongkere6913732012-07-03 15:03:04 -0700704
Koushik Dutta29706d12012-12-17 22:25:22 -0800705 // For signing .apks, use the maximum compression to make
706 // them as small as possible (since they live forever on
707 // the system partition). For OTA packages, use the
708 // default compression level, which is much much faster
709 // and produces output that is only a tiny bit larger
710 // (~0.1% on full OTA packages I tested).
Doug Zongkere6913732012-07-03 15:03:04 -0700711 outputJar.setLevel(9);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800712
Koushik Dutta29706d12012-12-17 22:25:22 -0800713 signFile(addDigestsToManifest(inputJar), inputJar,
714 publicKey, privateKey, outputJar);
715 outputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700716 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800717 } catch (Exception e) {
718 e.printStackTrace();
719 System.exit(1);
720 } finally {
721 try {
722 if (inputJar != null) inputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700723 if (outputFile != null) outputFile.close();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800724 } catch (IOException e) {
725 e.printStackTrace();
726 System.exit(1);
727 }
728 }
729 }
730}