blob: 5afb8d1061a0cee5391871f458093e6d8a1d6378 [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;
Alex Klyubin9667b182015-12-10 13:38:50 -080037import org.conscrypt.OpenSSLProvider;
The Android Open Source Project88b60792009-03-03 19:28:42 -080038
zhang jun22717f92014-07-10 16:34:57 +080039import java.io.Console;
The Android Open Source Project88b60792009-03-03 19:28:42 -080040import java.io.BufferedReader;
Kenny Root62ea4a52013-09-25 09:59:10 -070041import java.io.ByteArrayInputStream;
The Android Open Source Project88b60792009-03-03 19:28:42 -080042import java.io.ByteArrayOutputStream;
43import java.io.DataInputStream;
44import java.io.File;
45import java.io.FileInputStream;
46import java.io.FileOutputStream;
47import java.io.FilterOutputStream;
48import java.io.IOException;
49import java.io.InputStream;
50import java.io.InputStreamReader;
51import java.io.OutputStream;
52import java.io.PrintStream;
Kenny Root89c961a2013-09-25 11:14:33 -070053import java.lang.reflect.Constructor;
The Android Open Source Project88b60792009-03-03 19:28:42 -080054import java.security.DigestOutputStream;
55import java.security.GeneralSecurityException;
56import java.security.Key;
57import java.security.KeyFactory;
58import java.security.MessageDigest;
59import java.security.PrivateKey;
Doug Zongker147626e2012-09-04 13:32:13 -070060import java.security.Provider;
61import java.security.Security;
62import java.security.cert.CertificateEncodingException;
The Android Open Source Project88b60792009-03-03 19:28:42 -080063import java.security.cert.CertificateFactory;
64import java.security.cert.X509Certificate;
65import java.security.spec.InvalidKeySpecException;
The Android Open Source Project88b60792009-03-03 19:28:42 -080066import java.security.spec.PKCS8EncodedKeySpec;
67import java.util.ArrayList;
68import java.util.Collections;
The Android Open Source Project88b60792009-03-03 19:28:42 -080069import java.util.Enumeration;
Kenny Root3d2365c2013-09-19 12:49:36 -070070import java.util.Locale;
The Android Open Source Project88b60792009-03-03 19:28:42 -080071import java.util.Map;
72import java.util.TreeMap;
73import java.util.jar.Attributes;
74import java.util.jar.JarEntry;
75import java.util.jar.JarFile;
76import java.util.jar.JarOutputStream;
77import java.util.jar.Manifest;
Doug Zongkeraf482b62009-06-08 10:46:55 -070078import java.util.regex.Pattern;
The Android Open Source Project88b60792009-03-03 19:28:42 -080079import javax.crypto.Cipher;
80import javax.crypto.EncryptedPrivateKeyInfo;
81import javax.crypto.SecretKeyFactory;
82import javax.crypto.spec.PBEKeySpec;
83
84/**
Doug Zongker8562fd42013-04-10 09:19:32 -070085 * HISTORICAL NOTE:
86 *
87 * Prior to the keylimepie release, SignApk ignored the signature
88 * algorithm specified in the certificate and always used SHA1withRSA.
89 *
Kenny Root3d2365c2013-09-19 12:49:36 -070090 * Starting with JB-MR2, the platform supports SHA256withRSA, so we use
91 * the signature algorithm in the certificate to select which to use
92 * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported.
Doug Zongker8562fd42013-04-10 09:19:32 -070093 *
94 * Because there are old keys still in use whose certificate actually
95 * says "MD5withRSA", we treat these as though they say "SHA1withRSA"
96 * for compatibility with older releases. This can be changed by
97 * altering the getAlgorithm() function below.
98 */
99
100
101/**
Kenny Root3d2365c2013-09-19 12:49:36 -0700102 * Command line tool to sign JAR files (including APKs and OTA updates) in a way
103 * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or
104 * SHA-256 (see historical note).
The Android Open Source Project88b60792009-03-03 19:28:42 -0800105 */
106class SignApk {
107 private static final String CERT_SF_NAME = "META-INF/CERT.SF";
Kenny Root3d2365c2013-09-19 12:49:36 -0700108 private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
Doug Zongkerb14c9762012-10-15 17:10:13 -0700109 private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
Kenny Root3d2365c2013-09-19 12:49:36 -0700110 private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s";
The Android Open Source Project88b60792009-03-03 19:28:42 -0800111
Doug Zongker7bb04232012-05-11 09:20:50 -0700112 private static final String OTACERT_NAME = "META-INF/com/android/otacert";
113
Doug Zongker8562fd42013-04-10 09:19:32 -0700114 // bitmasks for which hash algorithms we need the manifest to include.
115 private static final int USE_SHA1 = 1;
116 private static final int USE_SHA256 = 2;
117
118 /**
119 * Return one of USE_SHA1 or USE_SHA256 according to the signature
120 * algorithm specified in the cert.
121 */
Kenny Root3d2365c2013-09-19 12:49:36 -0700122 private static int getDigestAlgorithm(X509Certificate cert) {
123 String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
124 if ("SHA1WITHRSA".equals(sigAlg) ||
125 "MD5WITHRSA".equals(sigAlg)) { // see "HISTORICAL NOTE" above.
Doug Zongker8562fd42013-04-10 09:19:32 -0700126 return USE_SHA1;
Kenny Root3d2365c2013-09-19 12:49:36 -0700127 } else if (sigAlg.startsWith("SHA256WITH")) {
Doug Zongker8562fd42013-04-10 09:19:32 -0700128 return USE_SHA256;
129 } else {
130 throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
131 "\" in cert [" + cert.getSubjectDN());
132 }
133 }
134
Kenny Root3d2365c2013-09-19 12:49:36 -0700135 /** Returns the expected signature algorithm for this key type. */
136 private static String getSignatureAlgorithm(X509Certificate cert) {
Kenny Root3d2365c2013-09-19 12:49:36 -0700137 String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
138 if ("RSA".equalsIgnoreCase(keyType)) {
139 if (getDigestAlgorithm(cert) == USE_SHA256) {
140 return "SHA256withRSA";
141 } else {
142 return "SHA1withRSA";
143 }
144 } else if ("EC".equalsIgnoreCase(keyType)) {
145 return "SHA256withECDSA";
146 } else {
147 throw new IllegalArgumentException("unsupported key type: " + keyType);
148 }
149 }
150
Doug Zongkeraf482b62009-06-08 10:46:55 -0700151 // Files matching this pattern are not copied to the output.
152 private static Pattern stripPattern =
Kenny Root3d2365c2013-09-19 12:49:36 -0700153 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
Doug Zongkerb14c9762012-10-15 17:10:13 -0700154 Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
Doug Zongkeraf482b62009-06-08 10:46:55 -0700155
The Android Open Source Project88b60792009-03-03 19:28:42 -0800156 private static X509Certificate readPublicKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800157 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800158 FileInputStream input = new FileInputStream(file);
159 try {
160 CertificateFactory cf = CertificateFactory.getInstance("X.509");
161 return (X509Certificate) cf.generateCertificate(input);
162 } finally {
163 input.close();
164 }
165 }
166
167 /**
adattatr50c7c5a2015-09-03 11:17:57 -0700168 * If a console doesn't exist, reads the password from stdin
169 * 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 -0800170 *
171 * @param keyFile The file containing the private key. Used to prompt the user.
172 */
173 private static String readPassword(File keyFile) {
zhang jun22717f92014-07-10 16:34:57 +0800174 Console console;
175 char[] pwd;
adattatr50c7c5a2015-09-03 11:17:57 -0700176 if ((console = System.console()) == null) {
177 System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
178 System.out.flush();
179 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
180 try {
181 return stdin.readLine();
182 } catch (IOException ex) {
183 return null;
184 }
zhang jun22717f92014-07-10 16:34:57 +0800185 } else {
adattatr50c7c5a2015-09-03 11:17:57 -0700186 if ((pwd = console.readPassword("[%s]", "Enter password for " + keyFile)) != null) {
187 return String.valueOf(pwd);
188 } else {
189 return null;
190 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800191 }
192 }
193
194 /**
Kenny Root62ea4a52013-09-25 09:59:10 -0700195 * Decrypt an encrypted PKCS#8 format private key.
The Android Open Source Project88b60792009-03-03 19:28:42 -0800196 *
197 * Based on ghstark's post on Aug 6, 2006 at
198 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
199 *
200 * @param encryptedPrivateKey The raw data of the private key
201 * @param keyFile The file containing the private key
202 */
Kenny Root62ea4a52013-09-25 09:59:10 -0700203 private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
Koushik Dutta29706d12012-12-17 22:25:22 -0800204 throws GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800205 EncryptedPrivateKeyInfo epkInfo;
206 try {
207 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
208 } catch (IOException ex) {
209 // Probably not an encrypted key.
210 return null;
211 }
212
213 char[] password = readPassword(keyFile).toCharArray();
214
215 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
216 Key key = skFactory.generateSecret(new PBEKeySpec(password));
217
218 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
219 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
220
221 try {
222 return epkInfo.getKeySpec(cipher);
223 } catch (InvalidKeySpecException ex) {
224 System.err.println("signapk: Password for " + keyFile + " may be bad.");
225 throw ex;
226 }
227 }
228
Kenny Root62ea4a52013-09-25 09:59:10 -0700229 /** Read a PKCS#8 format private key. */
The Android Open Source Project88b60792009-03-03 19:28:42 -0800230 private static PrivateKey readPrivateKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800231 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800232 DataInputStream input = new DataInputStream(new FileInputStream(file));
233 try {
234 byte[] bytes = new byte[(int) file.length()];
235 input.read(bytes);
236
Kenny Root62ea4a52013-09-25 09:59:10 -0700237 /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
238 PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800239 if (spec == null) {
240 spec = new PKCS8EncodedKeySpec(bytes);
241 }
242
Kenny Root62ea4a52013-09-25 09:59:10 -0700243 /*
244 * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
245 * OID and use that to construct a KeyFactory.
246 */
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800247 PrivateKeyInfo pki;
248 try (ASN1InputStream bIn =
249 new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()))) {
250 pki = PrivateKeyInfo.getInstance(bIn.readObject());
251 }
Kenny Root62ea4a52013-09-25 09:59:10 -0700252 String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
Kenny Root3d2365c2013-09-19 12:49:36 -0700253
Kenny Root62ea4a52013-09-25 09:59:10 -0700254 return KeyFactory.getInstance(algOid).generatePrivate(spec);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800255 } finally {
256 input.close();
257 }
258 }
259
Doug Zongker8562fd42013-04-10 09:19:32 -0700260 /**
261 * Add the hash(es) of every file to the manifest, creating it if
262 * necessary.
263 */
264 private static Manifest addDigestsToManifest(JarFile jar, int hashes)
Koushik Dutta29706d12012-12-17 22:25:22 -0800265 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800266 Manifest input = jar.getManifest();
267 Manifest output = new Manifest();
268 Attributes main = output.getMainAttributes();
269 if (input != null) {
270 main.putAll(input.getMainAttributes());
271 } else {
272 main.putValue("Manifest-Version", "1.0");
273 main.putValue("Created-By", "1.0 (Android SignApk)");
274 }
275
Doug Zongker8562fd42013-04-10 09:19:32 -0700276 MessageDigest md_sha1 = null;
277 MessageDigest md_sha256 = null;
278 if ((hashes & USE_SHA1) != 0) {
279 md_sha1 = MessageDigest.getInstance("SHA1");
280 }
281 if ((hashes & USE_SHA256) != 0) {
282 md_sha256 = MessageDigest.getInstance("SHA256");
283 }
284
The Android Open Source Project88b60792009-03-03 19:28:42 -0800285 byte[] buffer = new byte[4096];
286 int num;
287
288 // We sort the input entries by name, and add them to the
289 // output manifest in sorted order. We expect that the output
290 // map will be deterministic.
291
292 TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
293
294 for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
295 JarEntry entry = e.nextElement();
296 byName.put(entry.getName(), entry);
297 }
298
299 for (JarEntry entry: byName.values()) {
300 String name = entry.getName();
Doug Zongkerb14c9762012-10-15 17:10:13 -0700301 if (!entry.isDirectory() &&
302 (stripPattern == null || !stripPattern.matcher(name).matches())) {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800303 InputStream data = jar.getInputStream(entry);
304 while ((num = data.read(buffer)) > 0) {
Doug Zongker8562fd42013-04-10 09:19:32 -0700305 if (md_sha1 != null) md_sha1.update(buffer, 0, num);
306 if (md_sha256 != null) md_sha256.update(buffer, 0, num);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800307 }
308
309 Attributes attr = null;
310 if (input != null) attr = input.getAttributes(name);
311 attr = attr != null ? new Attributes(attr) : new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700312 if (md_sha1 != null) {
313 attr.putValue("SHA1-Digest",
314 new String(Base64.encode(md_sha1.digest()), "ASCII"));
315 }
316 if (md_sha256 != null) {
317 attr.putValue("SHA-256-Digest",
318 new String(Base64.encode(md_sha256.digest()), "ASCII"));
319 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800320 output.getEntries().put(name, attr);
321 }
322 }
323
324 return output;
325 }
326
Doug Zongker7bb04232012-05-11 09:20:50 -0700327 /**
328 * Add a copy of the public key to the archive; this should
329 * exactly match one of the files in
330 * /system/etc/security/otacerts.zip on the device. (The same
331 * cert can be extracted from the CERT.RSA file but this is much
332 * easier to get at.)
333 */
334 private static void addOtacert(JarOutputStream outputJar,
335 File publicKeyFile,
336 long timestamp,
Doug Zongker8562fd42013-04-10 09:19:32 -0700337 Manifest manifest,
338 int hash)
Doug Zongker7bb04232012-05-11 09:20:50 -0700339 throws IOException, GeneralSecurityException {
Doug Zongker8562fd42013-04-10 09:19:32 -0700340 MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256");
Doug Zongker7bb04232012-05-11 09:20:50 -0700341
342 JarEntry je = new JarEntry(OTACERT_NAME);
343 je.setTime(timestamp);
344 outputJar.putNextEntry(je);
345 FileInputStream input = new FileInputStream(publicKeyFile);
346 byte[] b = new byte[4096];
347 int read;
348 while ((read = input.read(b)) != -1) {
349 outputJar.write(b, 0, read);
350 md.update(b, 0, read);
351 }
352 input.close();
353
354 Attributes attr = new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700355 attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest",
Doug Zongker147626e2012-09-04 13:32:13 -0700356 new String(Base64.encode(md.digest()), "ASCII"));
Doug Zongker7bb04232012-05-11 09:20:50 -0700357 manifest.getEntries().put(OTACERT_NAME, attr);
358 }
359
360
Doug Zongker147626e2012-09-04 13:32:13 -0700361 /** Write to another stream and track how many bytes have been
362 * written.
363 */
364 private static class CountOutputStream extends FilterOutputStream {
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700365 private int mCount;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800366
Doug Zongker147626e2012-09-04 13:32:13 -0700367 public CountOutputStream(OutputStream out) {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800368 super(out);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700369 mCount = 0;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800370 }
371
372 @Override
373 public void write(int b) throws IOException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800374 super.write(b);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700375 mCount++;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800376 }
377
378 @Override
379 public void write(byte[] b, int off, int len) throws IOException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800380 super.write(b, off, len);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700381 mCount += len;
382 }
383
384 public int size() {
385 return mCount;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800386 }
387 }
388
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700389 /** Write a .SF file with a digest of the specified manifest. */
Doug Zongker8562fd42013-04-10 09:19:32 -0700390 private static void writeSignatureFile(Manifest manifest, OutputStream out,
391 int hash)
Doug Zongker147626e2012-09-04 13:32:13 -0700392 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800393 Manifest sf = new Manifest();
394 Attributes main = sf.getMainAttributes();
395 main.putValue("Signature-Version", "1.0");
396 main.putValue("Created-By", "1.0 (Android SignApk)");
397
Doug Zongker8562fd42013-04-10 09:19:32 -0700398 MessageDigest md = MessageDigest.getInstance(
399 hash == USE_SHA256 ? "SHA256" : "SHA1");
The Android Open Source Project88b60792009-03-03 19:28:42 -0800400 PrintStream print = new PrintStream(
Koushik Dutta29706d12012-12-17 22:25:22 -0800401 new DigestOutputStream(new ByteArrayOutputStream(), md),
402 true, "UTF-8");
The Android Open Source Project88b60792009-03-03 19:28:42 -0800403
404 // Digest of the entire manifest
405 manifest.write(print);
406 print.flush();
Doug Zongker8562fd42013-04-10 09:19:32 -0700407 main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
Doug Zongker147626e2012-09-04 13:32:13 -0700408 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800409
410 Map<String, Attributes> entries = manifest.getEntries();
411 for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
412 // Digest of the manifest stanza for this entry.
413 print.print("Name: " + entry.getKey() + "\r\n");
414 for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
415 print.print(att.getKey() + ": " + att.getValue() + "\r\n");
416 }
417 print.print("\r\n");
418 print.flush();
419
420 Attributes sfAttr = new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700421 sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest",
Doug Zongker147626e2012-09-04 13:32:13 -0700422 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800423 sf.getEntries().put(entry.getKey(), sfAttr);
424 }
425
Doug Zongker147626e2012-09-04 13:32:13 -0700426 CountOutputStream cout = new CountOutputStream(out);
427 sf.write(cout);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700428
429 // A bug in the java.util.jar implementation of Android platforms
430 // up to version 1.6 will cause a spurious IOException to be thrown
431 // if the length of the signature file is a multiple of 1024 bytes.
432 // As a workaround, add an extra CRLF in this case.
Doug Zongker147626e2012-09-04 13:32:13 -0700433 if ((cout.size() % 1024) == 0) {
434 cout.write('\r');
435 cout.write('\n');
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700436 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800437 }
438
Doug Zongker147626e2012-09-04 13:32:13 -0700439 /** Sign data and write the digital signature to 'out'. */
The Android Open Source Project88b60792009-03-03 19:28:42 -0800440 private static void writeSignatureBlock(
Doug Zongker147626e2012-09-04 13:32:13 -0700441 CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
442 OutputStream out)
443 throws IOException,
444 CertificateEncodingException,
445 OperatorCreationException,
446 CMSException {
447 ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
448 certList.add(publicKey);
449 JcaCertStore certs = new JcaCertStore(certList);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800450
Doug Zongker147626e2012-09-04 13:32:13 -0700451 CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
Kenny Root3d2365c2013-09-19 12:49:36 -0700452 ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
Doug Zongker147626e2012-09-04 13:32:13 -0700453 .build(privateKey);
454 gen.addSignerInfoGenerator(
455 new JcaSignerInfoGeneratorBuilder(
456 new JcaDigestCalculatorProviderBuilder()
Doug Zongker147626e2012-09-04 13:32:13 -0700457 .build())
458 .setDirectSignature(true)
Doug Zongker8562fd42013-04-10 09:19:32 -0700459 .build(signer, publicKey));
Doug Zongker147626e2012-09-04 13:32:13 -0700460 gen.addCertificates(certs);
461 CMSSignedData sigData = gen.generate(data, false);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800462
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800463 try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
464 DEROutputStream dos = new DEROutputStream(out);
465 dos.writeObject(asn1.readObject());
466 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800467 }
468
Koushik Dutta29706d12012-12-17 22:25:22 -0800469 /**
470 * Copy all the files in a manifest from input to output. We set
471 * the modification times in the output to a fixed time, so as to
472 * reduce variation in the output file and make incremental OTAs
473 * more efficient.
474 */
Doug Zongker1d67eec2014-05-15 09:54:26 -0700475 private static void copyFiles(Manifest manifest, JarFile in, JarOutputStream out,
Alex Klyubin924a6832015-12-02 19:02:01 -0800476 long timestamp, int defaultAlignment) throws IOException {
Koushik Dutta29706d12012-12-17 22:25:22 -0800477 byte[] buffer = new byte[4096];
478 int num;
479
480 Map<String, Attributes> entries = manifest.getEntries();
481 ArrayList<String> names = new ArrayList<String>(entries.keySet());
482 Collections.sort(names);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700483
484 boolean firstEntry = true;
485 long offset = 0L;
486
487 // We do the copy in two passes -- first copying all the
488 // entries that are STORED, then copying all the entries that
489 // have any other compression flag (which in practice means
490 // DEFLATED). This groups all the stored entries together at
491 // the start of the file and makes it easier to do alignment
492 // on them (since only stored entries are aligned).
493
Koushik Dutta29706d12012-12-17 22:25:22 -0800494 for (String name : names) {
495 JarEntry inEntry = in.getJarEntry(name);
496 JarEntry outEntry = null;
Doug Zongker1d67eec2014-05-15 09:54:26 -0700497 if (inEntry.getMethod() != JarEntry.STORED) continue;
498 // Preserve the STORED method of the input entry.
499 outEntry = new JarEntry(inEntry);
500 outEntry.setTime(timestamp);
501
502 // 'offset' is the offset into the file at which we expect
503 // the file data to begin. This is the value we need to
504 // make a multiple of 'alignement'.
505 offset += JarFile.LOCHDR + outEntry.getName().length();
506 if (firstEntry) {
507 // The first entry in a jar file has an extra field of
508 // four bytes that you can't get rid of; any extra
509 // data you specify in the JarEntry is appended to
510 // these forced four bytes. This is JAR_MAGIC in
511 // JarOutputStream; the bytes are 0xfeca0000.
512 offset += 4;
513 firstEntry = false;
Koushik Dutta29706d12012-12-17 22:25:22 -0800514 }
Alex Klyubin924a6832015-12-02 19:02:01 -0800515 int alignment = getStoredEntryDataAlignment(name, defaultAlignment);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700516 if (alignment > 0 && (offset % alignment != 0)) {
517 // Set the "extra data" of the entry to between 1 and
518 // alignment-1 bytes, to make the file data begin at
519 // an aligned offset.
520 int needed = alignment - (int)(offset % alignment);
521 outEntry.setExtra(new byte[needed]);
522 offset += needed;
523 }
524
525 out.putNextEntry(outEntry);
526
527 InputStream data = in.getInputStream(inEntry);
528 while ((num = data.read(buffer)) > 0) {
529 out.write(buffer, 0, num);
530 offset += num;
531 }
532 out.flush();
533 }
534
535 // Copy all the non-STORED entries. We don't attempt to
536 // maintain the 'offset' variable past this point; we don't do
537 // alignment on these entries.
538
539 for (String name : names) {
540 JarEntry inEntry = in.getJarEntry(name);
541 JarEntry outEntry = null;
542 if (inEntry.getMethod() == JarEntry.STORED) continue;
543 // Create a new entry so that the compressed len is recomputed.
544 outEntry = new JarEntry(name);
Koushik Dutta29706d12012-12-17 22:25:22 -0800545 outEntry.setTime(timestamp);
546 out.putNextEntry(outEntry);
547
548 InputStream data = in.getInputStream(inEntry);
549 while ((num = data.read(buffer)) > 0) {
550 out.write(buffer, 0, num);
551 }
552 out.flush();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700553 }
Koushik Dutta29706d12012-12-17 22:25:22 -0800554 }
555
Alex Klyubin924a6832015-12-02 19:02:01 -0800556 /**
557 * Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start
558 * relative to start of file or {@code 0} if alignment of this entry's data is not important.
559 */
560 private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment) {
561 if (defaultAlignment <= 0) {
562 return 0;
563 }
564
565 if (entryName.endsWith(".so")) {
566 // Align .so contents to memory page boundary to enable memory-mapped
567 // execution.
568 return 4096;
569 } else {
570 return defaultAlignment;
571 }
572 }
573
Koushik Dutta29706d12012-12-17 22:25:22 -0800574 private static class WholeFileSignerOutputStream extends FilterOutputStream {
575 private boolean closing = false;
576 private ByteArrayOutputStream footer = new ByteArrayOutputStream();
577 private OutputStream tee;
578
579 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
580 super(out);
581 this.tee = tee;
582 }
583
584 public void notifyClosing() {
585 closing = true;
586 }
587
588 public void finish() throws IOException {
589 closing = false;
590
591 byte[] data = footer.toByteArray();
592 if (data.length < 2)
593 throw new IOException("Less than two bytes written to footer");
594 write(data, 0, data.length - 2);
595 }
596
597 public byte[] getTail() {
598 return footer.toByteArray();
599 }
600
601 @Override
602 public void write(byte[] b) throws IOException {
603 write(b, 0, b.length);
604 }
605
606 @Override
607 public void write(byte[] b, int off, int len) throws IOException {
608 if (closing) {
609 // if the jar is about to close, save the footer that will be written
610 footer.write(b, off, len);
611 }
612 else {
613 // write to both output streams. out is the CMSTypedData signer and tee is the file.
614 out.write(b, off, len);
615 tee.write(b, off, len);
616 }
617 }
618
619 @Override
620 public void write(int b) throws IOException {
621 if (closing) {
622 // if the jar is about to close, save the footer that will be written
623 footer.write(b);
624 }
625 else {
626 // write to both output streams. out is the CMSTypedData signer and tee is the file.
627 out.write(b);
628 tee.write(b);
629 }
630 }
631 }
632
633 private static class CMSSigner implements CMSTypedData {
634 private JarFile inputJar;
635 private File publicKeyFile;
636 private X509Certificate publicKey;
637 private PrivateKey privateKey;
Koushik Dutta29706d12012-12-17 22:25:22 -0800638 private OutputStream outputStream;
639 private final ASN1ObjectIdentifier type;
640 private WholeFileSignerOutputStream signer;
641
642 public CMSSigner(JarFile inputJar, File publicKeyFile,
643 X509Certificate publicKey, PrivateKey privateKey,
644 OutputStream outputStream) {
645 this.inputJar = inputJar;
646 this.publicKeyFile = publicKeyFile;
647 this.publicKey = publicKey;
648 this.privateKey = privateKey;
649 this.outputStream = outputStream;
650 this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
651 }
652
Kenny Rootbda807d2014-08-07 12:02:54 -0700653 /**
654 * This should actually return byte[] or something similar, but nothing
655 * actually checks it currently.
656 */
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800657 @Override
Koushik Dutta29706d12012-12-17 22:25:22 -0800658 public Object getContent() {
Kenny Rootbda807d2014-08-07 12:02:54 -0700659 return this;
Koushik Dutta29706d12012-12-17 22:25:22 -0800660 }
661
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800662 @Override
Koushik Dutta29706d12012-12-17 22:25:22 -0800663 public ASN1ObjectIdentifier getContentType() {
664 return type;
665 }
666
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800667 @Override
Koushik Dutta29706d12012-12-17 22:25:22 -0800668 public void write(OutputStream out) throws IOException {
669 try {
670 signer = new WholeFileSignerOutputStream(out, outputStream);
671 JarOutputStream outputJar = new JarOutputStream(signer);
672
Kenny Root3d2365c2013-09-19 12:49:36 -0700673 int hash = getDigestAlgorithm(publicKey);
Doug Zongker8562fd42013-04-10 09:19:32 -0700674
675 // Assume the certificate is valid for at least an hour.
676 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
677
678 Manifest manifest = addDigestsToManifest(inputJar, hash);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700679 copyFiles(manifest, inputJar, outputJar, timestamp, 0);
Doug Zongker8562fd42013-04-10 09:19:32 -0700680 addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
681
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800682 signFile(manifest,
Koushik Dutta29706d12012-12-17 22:25:22 -0800683 new X509Certificate[]{ publicKey },
684 new PrivateKey[]{ privateKey },
685 outputJar);
Koushik Dutta29706d12012-12-17 22:25:22 -0800686
687 signer.notifyClosing();
688 outputJar.close();
689 signer.finish();
690 }
691 catch (Exception e) {
692 throw new IOException(e);
693 }
694 }
695
696 public void writeSignatureBlock(ByteArrayOutputStream temp)
697 throws IOException,
698 CertificateEncodingException,
699 OperatorCreationException,
700 CMSException {
701 SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
702 }
703
704 public WholeFileSignerOutputStream getSigner() {
705 return signer;
706 }
707 }
708
709 private static void signWholeFile(JarFile inputJar, File publicKeyFile,
710 X509Certificate publicKey, PrivateKey privateKey,
711 OutputStream outputStream) throws Exception {
712 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
713 publicKey, privateKey, outputStream);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700714
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700715 ByteArrayOutputStream temp = new ByteArrayOutputStream();
716
717 // put a readable message and a null char at the start of the
718 // archive comment, so that tools that display the comment
719 // (hopefully) show something sensible.
720 // TODO: anything more useful we can put in this message?
721 byte[] message = "signed by SignApk".getBytes("UTF-8");
722 temp.write(message);
723 temp.write(0);
Doug Zongker147626e2012-09-04 13:32:13 -0700724
Koushik Dutta29706d12012-12-17 22:25:22 -0800725 cmsOut.writeSignatureBlock(temp);
726
727 byte[] zipData = cmsOut.getSigner().getTail();
728
729 // For a zip with no archive comment, the
730 // end-of-central-directory record will be 22 bytes long, so
731 // we expect to find the EOCD marker 22 bytes from the end.
732 if (zipData[zipData.length-22] != 0x50 ||
733 zipData[zipData.length-21] != 0x4b ||
734 zipData[zipData.length-20] != 0x05 ||
735 zipData[zipData.length-19] != 0x06) {
736 throw new IllegalArgumentException("zip data already has an archive comment");
737 }
738
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700739 int total_size = temp.size() + 6;
740 if (total_size > 0xffff) {
741 throw new IllegalArgumentException("signature is too big for ZIP file comment");
742 }
743 // signature starts this many bytes from the end of the file
744 int signature_start = total_size - message.length - 1;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700745 temp.write(signature_start & 0xff);
746 temp.write((signature_start >> 8) & 0xff);
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700747 // Why the 0xff bytes? In a zip file with no archive comment,
748 // bytes [-6:-2] of the file are the little-endian offset from
749 // the start of the file to the central directory. So for the
750 // two high bytes to be 0xff 0xff, the archive would have to
Doug Zongker147626e2012-09-04 13:32:13 -0700751 // be nearly 4GB in size. So it's unlikely that a real
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700752 // commentless archive would have 0xffs here, and lets us tell
753 // an old signed archive from a new one.
754 temp.write(0xff);
755 temp.write(0xff);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700756 temp.write(total_size & 0xff);
757 temp.write((total_size >> 8) & 0xff);
758 temp.flush();
759
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700760 // Signature verification checks that the EOCD header is the
761 // last such sequence in the file (to avoid minzip finding a
762 // fake EOCD appended after the signature in its scan). The
763 // odds of producing this sequence by chance are very low, but
764 // let's catch it here if it does.
765 byte[] b = temp.toByteArray();
766 for (int i = 0; i < b.length-3; ++i) {
767 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
768 throw new IllegalArgumentException("found spurious EOCD header at " + i);
769 }
770 }
771
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700772 outputStream.write(total_size & 0xff);
773 outputStream.write((total_size >> 8) & 0xff);
774 temp.writeTo(outputStream);
775 }
776
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800777 private static void signFile(Manifest manifest,
Koushik Dutta29706d12012-12-17 22:25:22 -0800778 X509Certificate[] publicKey, PrivateKey[] privateKey,
779 JarOutputStream outputJar)
780 throws Exception {
781 // Assume the certificate is valid for at least an hour.
782 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800783
Koushik Dutta29706d12012-12-17 22:25:22 -0800784 // MANIFEST.MF
Doug Zongker8562fd42013-04-10 09:19:32 -0700785 JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
Koushik Dutta29706d12012-12-17 22:25:22 -0800786 je.setTime(timestamp);
787 outputJar.putNextEntry(je);
788 manifest.write(outputJar);
789
790 int numKeys = publicKey.length;
791 for (int k = 0; k < numKeys; ++k) {
792 // CERT.SF / CERT#.SF
793 je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
794 (String.format(CERT_SF_MULTI_NAME, k)));
795 je.setTime(timestamp);
796 outputJar.putNextEntry(je);
797 ByteArrayOutputStream baos = new ByteArrayOutputStream();
Kenny Root3d2365c2013-09-19 12:49:36 -0700798 writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
Koushik Dutta29706d12012-12-17 22:25:22 -0800799 byte[] signedData = baos.toByteArray();
800 outputJar.write(signedData);
801
Kenny Root3d2365c2013-09-19 12:49:36 -0700802 // CERT.{EC,RSA} / CERT#.{EC,RSA}
Kenny Root62ea4a52013-09-25 09:59:10 -0700803 final String keyType = publicKey[k].getPublicKey().getAlgorithm();
Kenny Root3d2365c2013-09-19 12:49:36 -0700804 je = new JarEntry(numKeys == 1 ?
Kenny Root62ea4a52013-09-25 09:59:10 -0700805 (String.format(CERT_SIG_NAME, keyType)) :
806 (String.format(CERT_SIG_MULTI_NAME, k, keyType)));
Koushik Dutta29706d12012-12-17 22:25:22 -0800807 je.setTime(timestamp);
808 outputJar.putNextEntry(je);
809 writeSignatureBlock(new CMSProcessableByteArray(signedData),
810 publicKey[k], privateKey[k], outputJar);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800811 }
812 }
813
Kenny Root89c961a2013-09-25 11:14:33 -0700814 /**
815 * Tries to load a JSE Provider by class name. This is for custom PrivateKey
816 * types that might be stored in PKCS#11-like storage.
817 */
818 private static void loadProviderIfNecessary(String providerClassName) {
819 if (providerClassName == null) {
820 return;
821 }
822
823 final Class<?> klass;
824 try {
825 final ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
826 if (sysLoader != null) {
827 klass = sysLoader.loadClass(providerClassName);
828 } else {
829 klass = Class.forName(providerClassName);
830 }
831 } catch (ClassNotFoundException e) {
832 e.printStackTrace();
833 System.exit(1);
834 return;
835 }
836
837 Constructor<?> constructor = null;
838 for (Constructor<?> c : klass.getConstructors()) {
839 if (c.getParameterTypes().length == 0) {
840 constructor = c;
841 break;
842 }
843 }
844 if (constructor == null) {
845 System.err.println("No zero-arg constructor found for " + providerClassName);
846 System.exit(1);
847 return;
848 }
849
850 final Object o;
851 try {
852 o = constructor.newInstance();
853 } catch (Exception e) {
854 e.printStackTrace();
855 System.exit(1);
856 return;
857 }
858 if (!(o instanceof Provider)) {
859 System.err.println("Not a Provider class: " + providerClassName);
860 System.exit(1);
861 }
862
863 Security.insertProviderAt((Provider) o, 1);
864 }
865
Doug Zongkerb14c9762012-10-15 17:10:13 -0700866 private static void usage() {
867 System.err.println("Usage: signapk [-w] " +
Doug Zongker1d67eec2014-05-15 09:54:26 -0700868 "[-a <alignment>] " +
Kenny Root89c961a2013-09-25 11:14:33 -0700869 "[-providerClass <className>] " +
Doug Zongkerb14c9762012-10-15 17:10:13 -0700870 "publickey.x509[.pem] privatekey.pk8 " +
871 "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
872 "input.jar output.jar");
873 System.exit(2);
874 }
875
The Android Open Source Project88b60792009-03-03 19:28:42 -0800876 public static void main(String[] args) {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700877 if (args.length < 4) usage();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800878
Alex Klyubin9667b182015-12-10 13:38:50 -0800879 // Install Conscrypt as the highest-priority provider. Its crypto primitives are faster than
880 // the standard or Bouncy Castle ones.
881 Security.insertProviderAt(new OpenSSLProvider(), 1);
882 // Install Bouncy Castle (as the lowest-priority provider) because Conscrypt does not offer
883 // DSA which may still be needed.
884 // TODO: Stop installing Bouncy Castle provider once DSA is no longer needed.
885 Security.addProvider(new BouncyCastleProvider());
Doug Zongker147626e2012-09-04 13:32:13 -0700886
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700887 boolean signWholeFile = false;
Kenny Root89c961a2013-09-25 11:14:33 -0700888 String providerClass = null;
Doug Zongker1d67eec2014-05-15 09:54:26 -0700889 int alignment = 4;
Kenny Root89c961a2013-09-25 11:14:33 -0700890
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700891 int argstart = 0;
Kenny Root89c961a2013-09-25 11:14:33 -0700892 while (argstart < args.length && args[argstart].startsWith("-")) {
893 if ("-w".equals(args[argstart])) {
894 signWholeFile = true;
895 ++argstart;
896 } else if ("-providerClass".equals(args[argstart])) {
897 if (argstart + 1 >= args.length) {
898 usage();
899 }
900 providerClass = args[++argstart];
901 ++argstart;
Doug Zongker1d67eec2014-05-15 09:54:26 -0700902 } else if ("-a".equals(args[argstart])) {
903 alignment = Integer.parseInt(args[++argstart]);
904 ++argstart;
Kenny Root89c961a2013-09-25 11:14:33 -0700905 } else {
906 usage();
907 }
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700908 }
909
Doug Zongkerb14c9762012-10-15 17:10:13 -0700910 if ((args.length - argstart) % 2 == 1) usage();
911 int numKeys = ((args.length - argstart) / 2) - 1;
912 if (signWholeFile && numKeys > 1) {
913 System.err.println("Only one key may be used with -w.");
914 System.exit(2);
915 }
916
Kenny Root89c961a2013-09-25 11:14:33 -0700917 loadProviderIfNecessary(providerClass);
918
Doug Zongkerb14c9762012-10-15 17:10:13 -0700919 String inputFilename = args[args.length-2];
920 String outputFilename = args[args.length-1];
921
The Android Open Source Project88b60792009-03-03 19:28:42 -0800922 JarFile inputJar = null;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700923 FileOutputStream outputFile = null;
Doug Zongker8562fd42013-04-10 09:19:32 -0700924 int hashes = 0;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800925
926 try {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700927 File firstPublicKeyFile = new File(args[argstart+0]);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800928
Doug Zongkerb14c9762012-10-15 17:10:13 -0700929 X509Certificate[] publicKey = new X509Certificate[numKeys];
Doug Zongker8562fd42013-04-10 09:19:32 -0700930 try {
931 for (int i = 0; i < numKeys; ++i) {
932 int argNum = argstart + i*2;
933 publicKey[i] = readPublicKey(new File(args[argNum]));
Kenny Root3d2365c2013-09-19 12:49:36 -0700934 hashes |= getDigestAlgorithm(publicKey[i]);
Doug Zongker8562fd42013-04-10 09:19:32 -0700935 }
936 } catch (IllegalArgumentException e) {
937 System.err.println(e);
938 System.exit(1);
Doug Zongkerb14c9762012-10-15 17:10:13 -0700939 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800940
Doug Zongkerb14c9762012-10-15 17:10:13 -0700941 // Set the ZIP file timestamp to the starting valid time
942 // of the 0th certificate plus one hour (to match what
943 // we've historically done).
944 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
945
946 PrivateKey[] privateKey = new PrivateKey[numKeys];
947 for (int i = 0; i < numKeys; ++i) {
948 int argNum = argstart + i*2 + 1;
949 privateKey[i] = readPrivateKey(new File(args[argNum]));
950 }
951 inputJar = new JarFile(new File(inputFilename), false); // Don't verify.
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700952
Koushik Dutta29706d12012-12-17 22:25:22 -0800953 outputFile = new FileOutputStream(outputFilename);
954
955
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700956 if (signWholeFile) {
Koushik Dutta29706d12012-12-17 22:25:22 -0800957 SignApk.signWholeFile(inputJar, firstPublicKeyFile,
958 publicKey[0], privateKey[0], outputFile);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700959 } else {
Koushik Dutta29706d12012-12-17 22:25:22 -0800960 JarOutputStream outputJar = new JarOutputStream(outputFile);
Doug Zongkere6913732012-07-03 15:03:04 -0700961
Koushik Dutta29706d12012-12-17 22:25:22 -0800962 // For signing .apks, use the maximum compression to make
963 // them as small as possible (since they live forever on
964 // the system partition). For OTA packages, use the
965 // default compression level, which is much much faster
966 // and produces output that is only a tiny bit larger
967 // (~0.1% on full OTA packages I tested).
Doug Zongkere6913732012-07-03 15:03:04 -0700968 outputJar.setLevel(9);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800969
Doug Zongker8562fd42013-04-10 09:19:32 -0700970 Manifest manifest = addDigestsToManifest(inputJar, hashes);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700971 copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800972 signFile(manifest, publicKey, privateKey, outputJar);
Koushik Dutta29706d12012-12-17 22:25:22 -0800973 outputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700974 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800975 } catch (Exception e) {
976 e.printStackTrace();
977 System.exit(1);
978 } finally {
979 try {
980 if (inputJar != null) inputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700981 if (outputFile != null) outputFile.close();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800982 } catch (IOException e) {
983 e.printStackTrace();
984 System.exit(1);
985 }
986 }
987 }
988}