blob: 397f8cca5521faf7db14a91f22e7d5ca13aeceff [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;
Kenny Root62ea4a52013-09-25 09:59:10 -070023import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
Doug Zongker147626e2012-09-04 13:32:13 -070024import org.bouncycastle.cert.jcajce.JcaCertStore;
25import org.bouncycastle.cms.CMSException;
26import org.bouncycastle.cms.CMSProcessableByteArray;
27import org.bouncycastle.cms.CMSSignedData;
28import org.bouncycastle.cms.CMSSignedDataGenerator;
29import org.bouncycastle.cms.CMSTypedData;
30import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
31import org.bouncycastle.jce.provider.BouncyCastleProvider;
32import org.bouncycastle.operator.ContentSigner;
33import org.bouncycastle.operator.OperatorCreationException;
34import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
35import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
36import org.bouncycastle.util.encoders.Base64;
The Android Open Source Project88b60792009-03-03 19:28:42 -080037
zhang jun22717f92014-07-10 16:34:57 +080038import java.io.Console;
The Android Open Source Project88b60792009-03-03 19:28:42 -080039import java.io.BufferedReader;
Kenny Root62ea4a52013-09-25 09:59:10 -070040import java.io.ByteArrayInputStream;
The Android Open Source Project88b60792009-03-03 19:28:42 -080041import java.io.ByteArrayOutputStream;
42import java.io.DataInputStream;
43import java.io.File;
44import java.io.FileInputStream;
45import java.io.FileOutputStream;
46import java.io.FilterOutputStream;
47import java.io.IOException;
48import java.io.InputStream;
49import java.io.InputStreamReader;
50import java.io.OutputStream;
51import java.io.PrintStream;
Kenny Root89c961a2013-09-25 11:14:33 -070052import java.lang.reflect.Constructor;
The Android Open Source Project88b60792009-03-03 19:28:42 -080053import java.security.DigestOutputStream;
54import java.security.GeneralSecurityException;
55import java.security.Key;
56import java.security.KeyFactory;
57import java.security.MessageDigest;
58import java.security.PrivateKey;
Doug Zongker147626e2012-09-04 13:32:13 -070059import java.security.Provider;
60import java.security.Security;
61import java.security.cert.CertificateEncodingException;
The Android Open Source Project88b60792009-03-03 19:28:42 -080062import java.security.cert.CertificateFactory;
63import java.security.cert.X509Certificate;
64import java.security.spec.InvalidKeySpecException;
The Android Open Source Project88b60792009-03-03 19:28:42 -080065import java.security.spec.PKCS8EncodedKeySpec;
66import java.util.ArrayList;
67import java.util.Collections;
The Android Open Source Project88b60792009-03-03 19:28:42 -080068import java.util.Enumeration;
Kenny Root3d2365c2013-09-19 12:49:36 -070069import java.util.Locale;
The Android Open Source Project88b60792009-03-03 19:28:42 -080070import java.util.Map;
71import java.util.TreeMap;
72import java.util.jar.Attributes;
73import java.util.jar.JarEntry;
74import java.util.jar.JarFile;
75import java.util.jar.JarOutputStream;
76import java.util.jar.Manifest;
Doug Zongkeraf482b62009-06-08 10:46:55 -070077import java.util.regex.Pattern;
The Android Open Source Project88b60792009-03-03 19:28:42 -080078import javax.crypto.Cipher;
79import javax.crypto.EncryptedPrivateKeyInfo;
80import javax.crypto.SecretKeyFactory;
81import javax.crypto.spec.PBEKeySpec;
82
83/**
Doug Zongker8562fd42013-04-10 09:19:32 -070084 * HISTORICAL NOTE:
85 *
86 * Prior to the keylimepie release, SignApk ignored the signature
87 * algorithm specified in the certificate and always used SHA1withRSA.
88 *
Kenny Root3d2365c2013-09-19 12:49:36 -070089 * Starting with JB-MR2, the platform supports SHA256withRSA, so we use
90 * the signature algorithm in the certificate to select which to use
91 * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported.
Doug Zongker8562fd42013-04-10 09:19:32 -070092 *
93 * Because there are old keys still in use whose certificate actually
94 * says "MD5withRSA", we treat these as though they say "SHA1withRSA"
95 * for compatibility with older releases. This can be changed by
96 * altering the getAlgorithm() function below.
97 */
98
99
100/**
Kenny Root3d2365c2013-09-19 12:49:36 -0700101 * Command line tool to sign JAR files (including APKs and OTA updates) in a way
102 * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or
103 * SHA-256 (see historical note).
The Android Open Source Project88b60792009-03-03 19:28:42 -0800104 */
105class SignApk {
106 private static final String CERT_SF_NAME = "META-INF/CERT.SF";
Kenny Root3d2365c2013-09-19 12:49:36 -0700107 private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
Doug Zongkerb14c9762012-10-15 17:10:13 -0700108 private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
Kenny Root3d2365c2013-09-19 12:49:36 -0700109 private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s";
The Android Open Source Project88b60792009-03-03 19:28:42 -0800110
Doug Zongker7bb04232012-05-11 09:20:50 -0700111 private static final String OTACERT_NAME = "META-INF/com/android/otacert";
112
Doug Zongker147626e2012-09-04 13:32:13 -0700113 private static Provider sBouncyCastleProvider;
114
Doug Zongker8562fd42013-04-10 09:19:32 -0700115 // bitmasks for which hash algorithms we need the manifest to include.
116 private static final int USE_SHA1 = 1;
117 private static final int USE_SHA256 = 2;
118
119 /**
120 * Return one of USE_SHA1 or USE_SHA256 according to the signature
121 * algorithm specified in the cert.
122 */
Kenny Root3d2365c2013-09-19 12:49:36 -0700123 private static int getDigestAlgorithm(X509Certificate cert) {
124 String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
125 if ("SHA1WITHRSA".equals(sigAlg) ||
126 "MD5WITHRSA".equals(sigAlg)) { // see "HISTORICAL NOTE" above.
Doug Zongker8562fd42013-04-10 09:19:32 -0700127 return USE_SHA1;
Kenny Root3d2365c2013-09-19 12:49:36 -0700128 } else if (sigAlg.startsWith("SHA256WITH")) {
Doug Zongker8562fd42013-04-10 09:19:32 -0700129 return USE_SHA256;
130 } else {
131 throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
132 "\" in cert [" + cert.getSubjectDN());
133 }
134 }
135
Kenny Root3d2365c2013-09-19 12:49:36 -0700136 /** Returns the expected signature algorithm for this key type. */
137 private static String getSignatureAlgorithm(X509Certificate cert) {
Kenny Root3d2365c2013-09-19 12:49:36 -0700138 String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
139 if ("RSA".equalsIgnoreCase(keyType)) {
140 if (getDigestAlgorithm(cert) == USE_SHA256) {
141 return "SHA256withRSA";
142 } else {
143 return "SHA1withRSA";
144 }
145 } else if ("EC".equalsIgnoreCase(keyType)) {
146 return "SHA256withECDSA";
147 } else {
148 throw new IllegalArgumentException("unsupported key type: " + keyType);
149 }
150 }
151
Doug Zongkeraf482b62009-06-08 10:46:55 -0700152 // Files matching this pattern are not copied to the output.
153 private static Pattern stripPattern =
Kenny Root3d2365c2013-09-19 12:49:36 -0700154 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
Doug Zongkerb14c9762012-10-15 17:10:13 -0700155 Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
Doug Zongkeraf482b62009-06-08 10:46:55 -0700156
The Android Open Source Project88b60792009-03-03 19:28:42 -0800157 private static X509Certificate readPublicKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800158 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800159 FileInputStream input = new FileInputStream(file);
160 try {
161 CertificateFactory cf = CertificateFactory.getInstance("X.509");
162 return (X509Certificate) cf.generateCertificate(input);
163 } finally {
164 input.close();
165 }
166 }
167
168 /**
adattatr50c7c5a2015-09-03 11:17:57 -0700169 * If a console doesn't exist, reads the password from stdin
170 * If a console exists, reads the password from console and returns it as a string.
The Android Open Source Project88b60792009-03-03 19:28:42 -0800171 *
172 * @param keyFile The file containing the private key. Used to prompt the user.
173 */
174 private static String readPassword(File keyFile) {
zhang jun22717f92014-07-10 16:34:57 +0800175 Console console;
176 char[] pwd;
adattatr50c7c5a2015-09-03 11:17:57 -0700177 if ((console = System.console()) == null) {
178 System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
179 System.out.flush();
180 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
181 try {
182 return stdin.readLine();
183 } catch (IOException ex) {
184 return null;
185 }
zhang jun22717f92014-07-10 16:34:57 +0800186 } else {
adattatr50c7c5a2015-09-03 11:17:57 -0700187 if ((pwd = console.readPassword("[%s]", "Enter password for " + keyFile)) != null) {
188 return String.valueOf(pwd);
189 } else {
190 return null;
191 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800192 }
193 }
194
195 /**
Kenny Root62ea4a52013-09-25 09:59:10 -0700196 * Decrypt an encrypted PKCS#8 format private key.
The Android Open Source Project88b60792009-03-03 19:28:42 -0800197 *
198 * Based on ghstark's post on Aug 6, 2006 at
199 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
200 *
201 * @param encryptedPrivateKey The raw data of the private key
202 * @param keyFile The file containing the private key
203 */
Kenny Root62ea4a52013-09-25 09:59:10 -0700204 private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
Koushik Dutta29706d12012-12-17 22:25:22 -0800205 throws GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800206 EncryptedPrivateKeyInfo epkInfo;
207 try {
208 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
209 } catch (IOException ex) {
210 // Probably not an encrypted key.
211 return null;
212 }
213
214 char[] password = readPassword(keyFile).toCharArray();
215
216 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
217 Key key = skFactory.generateSecret(new PBEKeySpec(password));
218
219 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
220 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
221
222 try {
223 return epkInfo.getKeySpec(cipher);
224 } catch (InvalidKeySpecException ex) {
225 System.err.println("signapk: Password for " + keyFile + " may be bad.");
226 throw ex;
227 }
228 }
229
Kenny Root62ea4a52013-09-25 09:59:10 -0700230 /** Read a PKCS#8 format private key. */
The Android Open Source Project88b60792009-03-03 19:28:42 -0800231 private static PrivateKey readPrivateKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800232 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800233 DataInputStream input = new DataInputStream(new FileInputStream(file));
234 try {
235 byte[] bytes = new byte[(int) file.length()];
236 input.read(bytes);
237
Kenny Root62ea4a52013-09-25 09:59:10 -0700238 /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
239 PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800240 if (spec == null) {
241 spec = new PKCS8EncodedKeySpec(bytes);
242 }
243
Kenny Root62ea4a52013-09-25 09:59:10 -0700244 /*
245 * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
246 * OID and use that to construct a KeyFactory.
247 */
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800248 PrivateKeyInfo pki;
249 try (ASN1InputStream bIn =
250 new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()))) {
251 pki = PrivateKeyInfo.getInstance(bIn.readObject());
252 }
Kenny Root62ea4a52013-09-25 09:59:10 -0700253 String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
Kenny Root3d2365c2013-09-19 12:49:36 -0700254
Kenny Root62ea4a52013-09-25 09:59:10 -0700255 return KeyFactory.getInstance(algOid).generatePrivate(spec);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800256 } finally {
257 input.close();
258 }
259 }
260
Doug Zongker8562fd42013-04-10 09:19:32 -0700261 /**
262 * Add the hash(es) of every file to the manifest, creating it if
263 * necessary.
264 */
265 private static Manifest addDigestsToManifest(JarFile jar, int hashes)
Koushik Dutta29706d12012-12-17 22:25:22 -0800266 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800267 Manifest input = jar.getManifest();
268 Manifest output = new Manifest();
269 Attributes main = output.getMainAttributes();
270 if (input != null) {
271 main.putAll(input.getMainAttributes());
272 } else {
273 main.putValue("Manifest-Version", "1.0");
274 main.putValue("Created-By", "1.0 (Android SignApk)");
275 }
276
Doug Zongker8562fd42013-04-10 09:19:32 -0700277 MessageDigest md_sha1 = null;
278 MessageDigest md_sha256 = null;
279 if ((hashes & USE_SHA1) != 0) {
280 md_sha1 = MessageDigest.getInstance("SHA1");
281 }
282 if ((hashes & USE_SHA256) != 0) {
283 md_sha256 = MessageDigest.getInstance("SHA256");
284 }
285
The Android Open Source Project88b60792009-03-03 19:28:42 -0800286 byte[] buffer = new byte[4096];
287 int num;
288
289 // We sort the input entries by name, and add them to the
290 // output manifest in sorted order. We expect that the output
291 // map will be deterministic.
292
293 TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
294
295 for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
296 JarEntry entry = e.nextElement();
297 byName.put(entry.getName(), entry);
298 }
299
300 for (JarEntry entry: byName.values()) {
301 String name = entry.getName();
Doug Zongkerb14c9762012-10-15 17:10:13 -0700302 if (!entry.isDirectory() &&
303 (stripPattern == null || !stripPattern.matcher(name).matches())) {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800304 InputStream data = jar.getInputStream(entry);
305 while ((num = data.read(buffer)) > 0) {
Doug Zongker8562fd42013-04-10 09:19:32 -0700306 if (md_sha1 != null) md_sha1.update(buffer, 0, num);
307 if (md_sha256 != null) md_sha256.update(buffer, 0, num);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800308 }
309
310 Attributes attr = null;
311 if (input != null) attr = input.getAttributes(name);
312 attr = attr != null ? new Attributes(attr) : new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700313 if (md_sha1 != null) {
314 attr.putValue("SHA1-Digest",
315 new String(Base64.encode(md_sha1.digest()), "ASCII"));
316 }
317 if (md_sha256 != null) {
318 attr.putValue("SHA-256-Digest",
319 new String(Base64.encode(md_sha256.digest()), "ASCII"));
320 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800321 output.getEntries().put(name, attr);
322 }
323 }
324
325 return output;
326 }
327
Doug Zongker7bb04232012-05-11 09:20:50 -0700328 /**
329 * Add a copy of the public key to the archive; this should
330 * exactly match one of the files in
331 * /system/etc/security/otacerts.zip on the device. (The same
332 * cert can be extracted from the CERT.RSA file but this is much
333 * easier to get at.)
334 */
335 private static void addOtacert(JarOutputStream outputJar,
336 File publicKeyFile,
337 long timestamp,
Doug Zongker8562fd42013-04-10 09:19:32 -0700338 Manifest manifest,
339 int hash)
Doug Zongker7bb04232012-05-11 09:20:50 -0700340 throws IOException, GeneralSecurityException {
Doug Zongker8562fd42013-04-10 09:19:32 -0700341 MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256");
Doug Zongker7bb04232012-05-11 09:20:50 -0700342
343 JarEntry je = new JarEntry(OTACERT_NAME);
344 je.setTime(timestamp);
345 outputJar.putNextEntry(je);
346 FileInputStream input = new FileInputStream(publicKeyFile);
347 byte[] b = new byte[4096];
348 int read;
349 while ((read = input.read(b)) != -1) {
350 outputJar.write(b, 0, read);
351 md.update(b, 0, read);
352 }
353 input.close();
354
355 Attributes attr = new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700356 attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest",
Doug Zongker147626e2012-09-04 13:32:13 -0700357 new String(Base64.encode(md.digest()), "ASCII"));
Doug Zongker7bb04232012-05-11 09:20:50 -0700358 manifest.getEntries().put(OTACERT_NAME, attr);
359 }
360
361
Doug Zongker147626e2012-09-04 13:32:13 -0700362 /** Write to another stream and track how many bytes have been
363 * written.
364 */
365 private static class CountOutputStream extends FilterOutputStream {
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700366 private int mCount;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800367
Doug Zongker147626e2012-09-04 13:32:13 -0700368 public CountOutputStream(OutputStream out) {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800369 super(out);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700370 mCount = 0;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800371 }
372
373 @Override
374 public void write(int b) throws IOException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800375 super.write(b);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700376 mCount++;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800377 }
378
379 @Override
380 public void write(byte[] b, int off, int len) throws IOException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800381 super.write(b, off, len);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700382 mCount += len;
383 }
384
385 public int size() {
386 return mCount;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800387 }
388 }
389
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700390 /** Write a .SF file with a digest of the specified manifest. */
Doug Zongker8562fd42013-04-10 09:19:32 -0700391 private static void writeSignatureFile(Manifest manifest, OutputStream out,
392 int hash)
Doug Zongker147626e2012-09-04 13:32:13 -0700393 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800394 Manifest sf = new Manifest();
395 Attributes main = sf.getMainAttributes();
396 main.putValue("Signature-Version", "1.0");
397 main.putValue("Created-By", "1.0 (Android SignApk)");
398
Doug Zongker8562fd42013-04-10 09:19:32 -0700399 MessageDigest md = MessageDigest.getInstance(
400 hash == USE_SHA256 ? "SHA256" : "SHA1");
The Android Open Source Project88b60792009-03-03 19:28:42 -0800401 PrintStream print = new PrintStream(
Koushik Dutta29706d12012-12-17 22:25:22 -0800402 new DigestOutputStream(new ByteArrayOutputStream(), md),
403 true, "UTF-8");
The Android Open Source Project88b60792009-03-03 19:28:42 -0800404
405 // Digest of the entire manifest
406 manifest.write(print);
407 print.flush();
Doug Zongker8562fd42013-04-10 09:19:32 -0700408 main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
Doug Zongker147626e2012-09-04 13:32:13 -0700409 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800410
411 Map<String, Attributes> entries = manifest.getEntries();
412 for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
413 // Digest of the manifest stanza for this entry.
414 print.print("Name: " + entry.getKey() + "\r\n");
415 for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
416 print.print(att.getKey() + ": " + att.getValue() + "\r\n");
417 }
418 print.print("\r\n");
419 print.flush();
420
421 Attributes sfAttr = new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700422 sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest",
Doug Zongker147626e2012-09-04 13:32:13 -0700423 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800424 sf.getEntries().put(entry.getKey(), sfAttr);
425 }
426
Doug Zongker147626e2012-09-04 13:32:13 -0700427 CountOutputStream cout = new CountOutputStream(out);
428 sf.write(cout);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700429
430 // A bug in the java.util.jar implementation of Android platforms
431 // up to version 1.6 will cause a spurious IOException to be thrown
432 // if the length of the signature file is a multiple of 1024 bytes.
433 // As a workaround, add an extra CRLF in this case.
Doug Zongker147626e2012-09-04 13:32:13 -0700434 if ((cout.size() % 1024) == 0) {
435 cout.write('\r');
436 cout.write('\n');
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700437 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800438 }
439
Doug Zongker147626e2012-09-04 13:32:13 -0700440 /** Sign data and write the digital signature to 'out'. */
The Android Open Source Project88b60792009-03-03 19:28:42 -0800441 private static void writeSignatureBlock(
Doug Zongker147626e2012-09-04 13:32:13 -0700442 CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
443 OutputStream out)
444 throws IOException,
445 CertificateEncodingException,
446 OperatorCreationException,
447 CMSException {
448 ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
449 certList.add(publicKey);
450 JcaCertStore certs = new JcaCertStore(certList);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800451
Doug Zongker147626e2012-09-04 13:32:13 -0700452 CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
Kenny Root3d2365c2013-09-19 12:49:36 -0700453 ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
Doug Zongker147626e2012-09-04 13:32:13 -0700454 .setProvider(sBouncyCastleProvider)
455 .build(privateKey);
456 gen.addSignerInfoGenerator(
457 new JcaSignerInfoGeneratorBuilder(
458 new JcaDigestCalculatorProviderBuilder()
459 .setProvider(sBouncyCastleProvider)
460 .build())
461 .setDirectSignature(true)
Doug Zongker8562fd42013-04-10 09:19:32 -0700462 .build(signer, publicKey));
Doug Zongker147626e2012-09-04 13:32:13 -0700463 gen.addCertificates(certs);
464 CMSSignedData sigData = gen.generate(data, false);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800465
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800466 try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
467 DEROutputStream dos = new DEROutputStream(out);
468 dos.writeObject(asn1.readObject());
469 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800470 }
471
Koushik Dutta29706d12012-12-17 22:25:22 -0800472 /**
473 * Copy all the files in a manifest from input to output. We set
474 * the modification times in the output to a fixed time, so as to
475 * reduce variation in the output file and make incremental OTAs
476 * more efficient.
477 */
Doug Zongker1d67eec2014-05-15 09:54:26 -0700478 private static void copyFiles(Manifest manifest, JarFile in, JarOutputStream out,
Alex Klyubin924a6832015-12-02 19:02:01 -0800479 long timestamp, int defaultAlignment) throws IOException {
Koushik Dutta29706d12012-12-17 22:25:22 -0800480 byte[] buffer = new byte[4096];
481 int num;
482
483 Map<String, Attributes> entries = manifest.getEntries();
484 ArrayList<String> names = new ArrayList<String>(entries.keySet());
485 Collections.sort(names);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700486
487 boolean firstEntry = true;
488 long offset = 0L;
489
490 // We do the copy in two passes -- first copying all the
491 // entries that are STORED, then copying all the entries that
492 // have any other compression flag (which in practice means
493 // DEFLATED). This groups all the stored entries together at
494 // the start of the file and makes it easier to do alignment
495 // on them (since only stored entries are aligned).
496
Koushik Dutta29706d12012-12-17 22:25:22 -0800497 for (String name : names) {
498 JarEntry inEntry = in.getJarEntry(name);
499 JarEntry outEntry = null;
Doug Zongker1d67eec2014-05-15 09:54:26 -0700500 if (inEntry.getMethod() != JarEntry.STORED) continue;
501 // Preserve the STORED method of the input entry.
502 outEntry = new JarEntry(inEntry);
503 outEntry.setTime(timestamp);
504
505 // 'offset' is the offset into the file at which we expect
506 // the file data to begin. This is the value we need to
507 // make a multiple of 'alignement'.
508 offset += JarFile.LOCHDR + outEntry.getName().length();
509 if (firstEntry) {
510 // The first entry in a jar file has an extra field of
511 // four bytes that you can't get rid of; any extra
512 // data you specify in the JarEntry is appended to
513 // these forced four bytes. This is JAR_MAGIC in
514 // JarOutputStream; the bytes are 0xfeca0000.
515 offset += 4;
516 firstEntry = false;
Koushik Dutta29706d12012-12-17 22:25:22 -0800517 }
Alex Klyubin924a6832015-12-02 19:02:01 -0800518 int alignment = getStoredEntryDataAlignment(name, defaultAlignment);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700519 if (alignment > 0 && (offset % alignment != 0)) {
520 // Set the "extra data" of the entry to between 1 and
521 // alignment-1 bytes, to make the file data begin at
522 // an aligned offset.
523 int needed = alignment - (int)(offset % alignment);
524 outEntry.setExtra(new byte[needed]);
525 offset += needed;
526 }
527
528 out.putNextEntry(outEntry);
529
530 InputStream data = in.getInputStream(inEntry);
531 while ((num = data.read(buffer)) > 0) {
532 out.write(buffer, 0, num);
533 offset += num;
534 }
535 out.flush();
536 }
537
538 // Copy all the non-STORED entries. We don't attempt to
539 // maintain the 'offset' variable past this point; we don't do
540 // alignment on these entries.
541
542 for (String name : names) {
543 JarEntry inEntry = in.getJarEntry(name);
544 JarEntry outEntry = null;
545 if (inEntry.getMethod() == JarEntry.STORED) continue;
546 // Create a new entry so that the compressed len is recomputed.
547 outEntry = new JarEntry(name);
Koushik Dutta29706d12012-12-17 22:25:22 -0800548 outEntry.setTime(timestamp);
549 out.putNextEntry(outEntry);
550
551 InputStream data = in.getInputStream(inEntry);
552 while ((num = data.read(buffer)) > 0) {
553 out.write(buffer, 0, num);
554 }
555 out.flush();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700556 }
Koushik Dutta29706d12012-12-17 22:25:22 -0800557 }
558
Alex Klyubin924a6832015-12-02 19:02:01 -0800559 /**
560 * Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start
561 * relative to start of file or {@code 0} if alignment of this entry's data is not important.
562 */
563 private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment) {
564 if (defaultAlignment <= 0) {
565 return 0;
566 }
567
568 if (entryName.endsWith(".so")) {
569 // Align .so contents to memory page boundary to enable memory-mapped
570 // execution.
571 return 4096;
572 } else {
573 return defaultAlignment;
574 }
575 }
576
Koushik Dutta29706d12012-12-17 22:25:22 -0800577 private static class WholeFileSignerOutputStream extends FilterOutputStream {
578 private boolean closing = false;
579 private ByteArrayOutputStream footer = new ByteArrayOutputStream();
580 private OutputStream tee;
581
582 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
583 super(out);
584 this.tee = tee;
585 }
586
587 public void notifyClosing() {
588 closing = true;
589 }
590
591 public void finish() throws IOException {
592 closing = false;
593
594 byte[] data = footer.toByteArray();
595 if (data.length < 2)
596 throw new IOException("Less than two bytes written to footer");
597 write(data, 0, data.length - 2);
598 }
599
600 public byte[] getTail() {
601 return footer.toByteArray();
602 }
603
604 @Override
605 public void write(byte[] b) throws IOException {
606 write(b, 0, b.length);
607 }
608
609 @Override
610 public void write(byte[] b, int off, int len) throws IOException {
611 if (closing) {
612 // if the jar is about to close, save the footer that will be written
613 footer.write(b, off, len);
614 }
615 else {
616 // write to both output streams. out is the CMSTypedData signer and tee is the file.
617 out.write(b, off, len);
618 tee.write(b, off, len);
619 }
620 }
621
622 @Override
623 public void write(int b) throws IOException {
624 if (closing) {
625 // if the jar is about to close, save the footer that will be written
626 footer.write(b);
627 }
628 else {
629 // write to both output streams. out is the CMSTypedData signer and tee is the file.
630 out.write(b);
631 tee.write(b);
632 }
633 }
634 }
635
636 private static class CMSSigner implements CMSTypedData {
637 private JarFile inputJar;
638 private File publicKeyFile;
639 private X509Certificate publicKey;
640 private PrivateKey privateKey;
Koushik Dutta29706d12012-12-17 22:25:22 -0800641 private OutputStream outputStream;
642 private final ASN1ObjectIdentifier type;
643 private WholeFileSignerOutputStream signer;
644
645 public CMSSigner(JarFile inputJar, File publicKeyFile,
646 X509Certificate publicKey, PrivateKey privateKey,
647 OutputStream outputStream) {
648 this.inputJar = inputJar;
649 this.publicKeyFile = publicKeyFile;
650 this.publicKey = publicKey;
651 this.privateKey = privateKey;
652 this.outputStream = outputStream;
653 this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
654 }
655
Kenny Rootbda807d2014-08-07 12:02:54 -0700656 /**
657 * This should actually return byte[] or something similar, but nothing
658 * actually checks it currently.
659 */
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800660 @Override
Koushik Dutta29706d12012-12-17 22:25:22 -0800661 public Object getContent() {
Kenny Rootbda807d2014-08-07 12:02:54 -0700662 return this;
Koushik Dutta29706d12012-12-17 22:25:22 -0800663 }
664
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800665 @Override
Koushik Dutta29706d12012-12-17 22:25:22 -0800666 public ASN1ObjectIdentifier getContentType() {
667 return type;
668 }
669
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800670 @Override
Koushik Dutta29706d12012-12-17 22:25:22 -0800671 public void write(OutputStream out) throws IOException {
672 try {
673 signer = new WholeFileSignerOutputStream(out, outputStream);
674 JarOutputStream outputJar = new JarOutputStream(signer);
675
Kenny Root3d2365c2013-09-19 12:49:36 -0700676 int hash = getDigestAlgorithm(publicKey);
Doug Zongker8562fd42013-04-10 09:19:32 -0700677
678 // Assume the certificate is valid for at least an hour.
679 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
680
681 Manifest manifest = addDigestsToManifest(inputJar, hash);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700682 copyFiles(manifest, inputJar, outputJar, timestamp, 0);
Doug Zongker8562fd42013-04-10 09:19:32 -0700683 addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
684
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800685 signFile(manifest,
Koushik Dutta29706d12012-12-17 22:25:22 -0800686 new X509Certificate[]{ publicKey },
687 new PrivateKey[]{ privateKey },
688 outputJar);
Koushik Dutta29706d12012-12-17 22:25:22 -0800689
690 signer.notifyClosing();
691 outputJar.close();
692 signer.finish();
693 }
694 catch (Exception e) {
695 throw new IOException(e);
696 }
697 }
698
699 public void writeSignatureBlock(ByteArrayOutputStream temp)
700 throws IOException,
701 CertificateEncodingException,
702 OperatorCreationException,
703 CMSException {
704 SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
705 }
706
707 public WholeFileSignerOutputStream getSigner() {
708 return signer;
709 }
710 }
711
712 private static void signWholeFile(JarFile inputJar, File publicKeyFile,
713 X509Certificate publicKey, PrivateKey privateKey,
714 OutputStream outputStream) throws Exception {
715 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
716 publicKey, privateKey, outputStream);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700717
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700718 ByteArrayOutputStream temp = new ByteArrayOutputStream();
719
720 // put a readable message and a null char at the start of the
721 // archive comment, so that tools that display the comment
722 // (hopefully) show something sensible.
723 // TODO: anything more useful we can put in this message?
724 byte[] message = "signed by SignApk".getBytes("UTF-8");
725 temp.write(message);
726 temp.write(0);
Doug Zongker147626e2012-09-04 13:32:13 -0700727
Koushik Dutta29706d12012-12-17 22:25:22 -0800728 cmsOut.writeSignatureBlock(temp);
729
730 byte[] zipData = cmsOut.getSigner().getTail();
731
732 // For a zip with no archive comment, the
733 // end-of-central-directory record will be 22 bytes long, so
734 // we expect to find the EOCD marker 22 bytes from the end.
735 if (zipData[zipData.length-22] != 0x50 ||
736 zipData[zipData.length-21] != 0x4b ||
737 zipData[zipData.length-20] != 0x05 ||
738 zipData[zipData.length-19] != 0x06) {
739 throw new IllegalArgumentException("zip data already has an archive comment");
740 }
741
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700742 int total_size = temp.size() + 6;
743 if (total_size > 0xffff) {
744 throw new IllegalArgumentException("signature is too big for ZIP file comment");
745 }
746 // signature starts this many bytes from the end of the file
747 int signature_start = total_size - message.length - 1;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700748 temp.write(signature_start & 0xff);
749 temp.write((signature_start >> 8) & 0xff);
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700750 // Why the 0xff bytes? In a zip file with no archive comment,
751 // bytes [-6:-2] of the file are the little-endian offset from
752 // the start of the file to the central directory. So for the
753 // two high bytes to be 0xff 0xff, the archive would have to
Doug Zongker147626e2012-09-04 13:32:13 -0700754 // be nearly 4GB in size. So it's unlikely that a real
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700755 // commentless archive would have 0xffs here, and lets us tell
756 // an old signed archive from a new one.
757 temp.write(0xff);
758 temp.write(0xff);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700759 temp.write(total_size & 0xff);
760 temp.write((total_size >> 8) & 0xff);
761 temp.flush();
762
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700763 // Signature verification checks that the EOCD header is the
764 // last such sequence in the file (to avoid minzip finding a
765 // fake EOCD appended after the signature in its scan). The
766 // odds of producing this sequence by chance are very low, but
767 // let's catch it here if it does.
768 byte[] b = temp.toByteArray();
769 for (int i = 0; i < b.length-3; ++i) {
770 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
771 throw new IllegalArgumentException("found spurious EOCD header at " + i);
772 }
773 }
774
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700775 outputStream.write(total_size & 0xff);
776 outputStream.write((total_size >> 8) & 0xff);
777 temp.writeTo(outputStream);
778 }
779
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800780 private static void signFile(Manifest manifest,
Koushik Dutta29706d12012-12-17 22:25:22 -0800781 X509Certificate[] publicKey, PrivateKey[] privateKey,
782 JarOutputStream outputJar)
783 throws Exception {
784 // Assume the certificate is valid for at least an hour.
785 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800786
Koushik Dutta29706d12012-12-17 22:25:22 -0800787 // MANIFEST.MF
Doug Zongker8562fd42013-04-10 09:19:32 -0700788 JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
Koushik Dutta29706d12012-12-17 22:25:22 -0800789 je.setTime(timestamp);
790 outputJar.putNextEntry(je);
791 manifest.write(outputJar);
792
793 int numKeys = publicKey.length;
794 for (int k = 0; k < numKeys; ++k) {
795 // CERT.SF / CERT#.SF
796 je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
797 (String.format(CERT_SF_MULTI_NAME, k)));
798 je.setTime(timestamp);
799 outputJar.putNextEntry(je);
800 ByteArrayOutputStream baos = new ByteArrayOutputStream();
Kenny Root3d2365c2013-09-19 12:49:36 -0700801 writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
Koushik Dutta29706d12012-12-17 22:25:22 -0800802 byte[] signedData = baos.toByteArray();
803 outputJar.write(signedData);
804
Kenny Root3d2365c2013-09-19 12:49:36 -0700805 // CERT.{EC,RSA} / CERT#.{EC,RSA}
Kenny Root62ea4a52013-09-25 09:59:10 -0700806 final String keyType = publicKey[k].getPublicKey().getAlgorithm();
Kenny Root3d2365c2013-09-19 12:49:36 -0700807 je = new JarEntry(numKeys == 1 ?
Kenny Root62ea4a52013-09-25 09:59:10 -0700808 (String.format(CERT_SIG_NAME, keyType)) :
809 (String.format(CERT_SIG_MULTI_NAME, k, keyType)));
Koushik Dutta29706d12012-12-17 22:25:22 -0800810 je.setTime(timestamp);
811 outputJar.putNextEntry(je);
812 writeSignatureBlock(new CMSProcessableByteArray(signedData),
813 publicKey[k], privateKey[k], outputJar);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800814 }
815 }
816
Kenny Root89c961a2013-09-25 11:14:33 -0700817 /**
818 * Tries to load a JSE Provider by class name. This is for custom PrivateKey
819 * types that might be stored in PKCS#11-like storage.
820 */
821 private static void loadProviderIfNecessary(String providerClassName) {
822 if (providerClassName == null) {
823 return;
824 }
825
826 final Class<?> klass;
827 try {
828 final ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
829 if (sysLoader != null) {
830 klass = sysLoader.loadClass(providerClassName);
831 } else {
832 klass = Class.forName(providerClassName);
833 }
834 } catch (ClassNotFoundException e) {
835 e.printStackTrace();
836 System.exit(1);
837 return;
838 }
839
840 Constructor<?> constructor = null;
841 for (Constructor<?> c : klass.getConstructors()) {
842 if (c.getParameterTypes().length == 0) {
843 constructor = c;
844 break;
845 }
846 }
847 if (constructor == null) {
848 System.err.println("No zero-arg constructor found for " + providerClassName);
849 System.exit(1);
850 return;
851 }
852
853 final Object o;
854 try {
855 o = constructor.newInstance();
856 } catch (Exception e) {
857 e.printStackTrace();
858 System.exit(1);
859 return;
860 }
861 if (!(o instanceof Provider)) {
862 System.err.println("Not a Provider class: " + providerClassName);
863 System.exit(1);
864 }
865
866 Security.insertProviderAt((Provider) o, 1);
867 }
868
Doug Zongkerb14c9762012-10-15 17:10:13 -0700869 private static void usage() {
870 System.err.println("Usage: signapk [-w] " +
Doug Zongker1d67eec2014-05-15 09:54:26 -0700871 "[-a <alignment>] " +
Kenny Root89c961a2013-09-25 11:14:33 -0700872 "[-providerClass <className>] " +
Doug Zongkerb14c9762012-10-15 17:10:13 -0700873 "publickey.x509[.pem] privatekey.pk8 " +
874 "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
875 "input.jar output.jar");
876 System.exit(2);
877 }
878
The Android Open Source Project88b60792009-03-03 19:28:42 -0800879 public static void main(String[] args) {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700880 if (args.length < 4) usage();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800881
Doug Zongker147626e2012-09-04 13:32:13 -0700882 sBouncyCastleProvider = new BouncyCastleProvider();
883 Security.addProvider(sBouncyCastleProvider);
884
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700885 boolean signWholeFile = false;
Kenny Root89c961a2013-09-25 11:14:33 -0700886 String providerClass = null;
Doug Zongker1d67eec2014-05-15 09:54:26 -0700887 int alignment = 4;
Kenny Root89c961a2013-09-25 11:14:33 -0700888
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700889 int argstart = 0;
Kenny Root89c961a2013-09-25 11:14:33 -0700890 while (argstart < args.length && args[argstart].startsWith("-")) {
891 if ("-w".equals(args[argstart])) {
892 signWholeFile = true;
893 ++argstart;
894 } else if ("-providerClass".equals(args[argstart])) {
895 if (argstart + 1 >= args.length) {
896 usage();
897 }
898 providerClass = args[++argstart];
899 ++argstart;
Doug Zongker1d67eec2014-05-15 09:54:26 -0700900 } else if ("-a".equals(args[argstart])) {
901 alignment = Integer.parseInt(args[++argstart]);
902 ++argstart;
Kenny Root89c961a2013-09-25 11:14:33 -0700903 } else {
904 usage();
905 }
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700906 }
907
Doug Zongkerb14c9762012-10-15 17:10:13 -0700908 if ((args.length - argstart) % 2 == 1) usage();
909 int numKeys = ((args.length - argstart) / 2) - 1;
910 if (signWholeFile && numKeys > 1) {
911 System.err.println("Only one key may be used with -w.");
912 System.exit(2);
913 }
914
Kenny Root89c961a2013-09-25 11:14:33 -0700915 loadProviderIfNecessary(providerClass);
916
Doug Zongkerb14c9762012-10-15 17:10:13 -0700917 String inputFilename = args[args.length-2];
918 String outputFilename = args[args.length-1];
919
The Android Open Source Project88b60792009-03-03 19:28:42 -0800920 JarFile inputJar = null;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700921 FileOutputStream outputFile = null;
Doug Zongker8562fd42013-04-10 09:19:32 -0700922 int hashes = 0;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800923
924 try {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700925 File firstPublicKeyFile = new File(args[argstart+0]);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800926
Doug Zongkerb14c9762012-10-15 17:10:13 -0700927 X509Certificate[] publicKey = new X509Certificate[numKeys];
Doug Zongker8562fd42013-04-10 09:19:32 -0700928 try {
929 for (int i = 0; i < numKeys; ++i) {
930 int argNum = argstart + i*2;
931 publicKey[i] = readPublicKey(new File(args[argNum]));
Kenny Root3d2365c2013-09-19 12:49:36 -0700932 hashes |= getDigestAlgorithm(publicKey[i]);
Doug Zongker8562fd42013-04-10 09:19:32 -0700933 }
934 } catch (IllegalArgumentException e) {
935 System.err.println(e);
936 System.exit(1);
Doug Zongkerb14c9762012-10-15 17:10:13 -0700937 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800938
Doug Zongkerb14c9762012-10-15 17:10:13 -0700939 // Set the ZIP file timestamp to the starting valid time
940 // of the 0th certificate plus one hour (to match what
941 // we've historically done).
942 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
943
944 PrivateKey[] privateKey = new PrivateKey[numKeys];
945 for (int i = 0; i < numKeys; ++i) {
946 int argNum = argstart + i*2 + 1;
947 privateKey[i] = readPrivateKey(new File(args[argNum]));
948 }
949 inputJar = new JarFile(new File(inputFilename), false); // Don't verify.
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700950
Koushik Dutta29706d12012-12-17 22:25:22 -0800951 outputFile = new FileOutputStream(outputFilename);
952
953
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700954 if (signWholeFile) {
Koushik Dutta29706d12012-12-17 22:25:22 -0800955 SignApk.signWholeFile(inputJar, firstPublicKeyFile,
956 publicKey[0], privateKey[0], outputFile);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700957 } else {
Koushik Dutta29706d12012-12-17 22:25:22 -0800958 JarOutputStream outputJar = new JarOutputStream(outputFile);
Doug Zongkere6913732012-07-03 15:03:04 -0700959
Koushik Dutta29706d12012-12-17 22:25:22 -0800960 // For signing .apks, use the maximum compression to make
961 // them as small as possible (since they live forever on
962 // the system partition). For OTA packages, use the
963 // default compression level, which is much much faster
964 // and produces output that is only a tiny bit larger
965 // (~0.1% on full OTA packages I tested).
Doug Zongkere6913732012-07-03 15:03:04 -0700966 outputJar.setLevel(9);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800967
Doug Zongker8562fd42013-04-10 09:19:32 -0700968 Manifest manifest = addDigestsToManifest(inputJar, hashes);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700969 copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800970 signFile(manifest, publicKey, privateKey, outputJar);
Koushik Dutta29706d12012-12-17 22:25:22 -0800971 outputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700972 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800973 } catch (Exception e) {
974 e.printStackTrace();
975 System.exit(1);
976 } finally {
977 try {
978 if (inputJar != null) inputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700979 if (outputFile != null) outputFile.close();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800980 } catch (IOException e) {
981 e.printStackTrace();
982 System.exit(1);
983 }
984 }
985 }
986}