blob: 716ea3b74c22ba260663a5e54180f7015adfc5a5 [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/**
Doug Zongkerc0581a02013-04-10 09:19:32 -070081 * HISTORICAL NOTE:
82 *
83 * Prior to the keylimepie release, SignApk ignored the signature
84 * algorithm specified in the certificate and always used SHA1withRSA.
85 *
86 * Starting with keylimepie, we support SHA256withRSA, and use the
87 * signature algorithm in the certificate to select which to use
88 * (SHA256withRSA or SHA1withRSA).
89 *
90 * Because there are old keys still in use whose certificate actually
91 * says "MD5withRSA", we treat these as though they say "SHA1withRSA"
92 * for compatibility with older releases. This can be changed by
93 * altering the getAlgorithm() function below.
94 */
95
96
97/**
98 * Command line tool to sign JAR files (including APKs and OTA
99 * updates) in a way compatible with the mincrypt verifier, using RSA
100 * keys and SHA1 or SHA-256.
The Android Open Source Project88b60792009-03-03 19:28:42 -0800101 */
102class SignApk {
103 private static final String CERT_SF_NAME = "META-INF/CERT.SF";
104 private static final String CERT_RSA_NAME = "META-INF/CERT.RSA";
Doug Zongkerb14c9762012-10-15 17:10:13 -0700105 private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
106 private static final String CERT_RSA_MULTI_NAME = "META-INF/CERT%d.RSA";
The Android Open Source Project88b60792009-03-03 19:28:42 -0800107
Doug Zongker7bb04232012-05-11 09:20:50 -0700108 private static final String OTACERT_NAME = "META-INF/com/android/otacert";
109
Doug Zongker147626e2012-09-04 13:32:13 -0700110 private static Provider sBouncyCastleProvider;
111
Doug Zongkerc0581a02013-04-10 09:19:32 -0700112 // bitmasks for which hash algorithms we need the manifest to include.
113 private static final int USE_SHA1 = 1;
114 private static final int USE_SHA256 = 2;
115
116 /**
117 * Return one of USE_SHA1 or USE_SHA256 according to the signature
118 * algorithm specified in the cert.
119 */
120 private static int getAlgorithm(X509Certificate cert) {
121 String sigAlg = cert.getSigAlgName();
122 if ("SHA1withRSA".equals(sigAlg) ||
123 "MD5withRSA".equals(sigAlg)) { // see "HISTORICAL NOTE" above.
124 return USE_SHA1;
125 } else if ("SHA256withRSA".equals(sigAlg)) {
126 return USE_SHA256;
127 } else {
128 throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
129 "\" in cert [" + cert.getSubjectDN());
130 }
131 }
132
Doug Zongkeraf482b62009-06-08 10:46:55 -0700133 // Files matching this pattern are not copied to the output.
134 private static Pattern stripPattern =
Doug Zongkerb14c9762012-10-15 17:10:13 -0700135 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA)|com/android/otacert))|(" +
136 Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
Doug Zongkeraf482b62009-06-08 10:46:55 -0700137
The Android Open Source Project88b60792009-03-03 19:28:42 -0800138 private static X509Certificate readPublicKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800139 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800140 FileInputStream input = new FileInputStream(file);
141 try {
142 CertificateFactory cf = CertificateFactory.getInstance("X.509");
143 return (X509Certificate) cf.generateCertificate(input);
144 } finally {
145 input.close();
146 }
147 }
148
149 /**
150 * Reads the password from stdin and returns it as a string.
151 *
152 * @param keyFile The file containing the private key. Used to prompt the user.
153 */
154 private static String readPassword(File keyFile) {
155 // TODO: use Console.readPassword() when it's available.
156 System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
157 System.out.flush();
158 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
159 try {
160 return stdin.readLine();
161 } catch (IOException ex) {
162 return null;
163 }
164 }
165
166 /**
167 * Decrypt an encrypted PKCS 8 format private key.
168 *
169 * Based on ghstark's post on Aug 6, 2006 at
170 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
171 *
172 * @param encryptedPrivateKey The raw data of the private key
173 * @param keyFile The file containing the private key
174 */
175 private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
Koushik Dutta29706d12012-12-17 22:25:22 -0800176 throws GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800177 EncryptedPrivateKeyInfo epkInfo;
178 try {
179 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
180 } catch (IOException ex) {
181 // Probably not an encrypted key.
182 return null;
183 }
184
185 char[] password = readPassword(keyFile).toCharArray();
186
187 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
188 Key key = skFactory.generateSecret(new PBEKeySpec(password));
189
190 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
191 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
192
193 try {
194 return epkInfo.getKeySpec(cipher);
195 } catch (InvalidKeySpecException ex) {
196 System.err.println("signapk: Password for " + keyFile + " may be bad.");
197 throw ex;
198 }
199 }
200
201 /** Read a PKCS 8 format private key. */
202 private static PrivateKey readPrivateKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800203 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800204 DataInputStream input = new DataInputStream(new FileInputStream(file));
205 try {
206 byte[] bytes = new byte[(int) file.length()];
207 input.read(bytes);
208
209 KeySpec spec = decryptPrivateKey(bytes, file);
210 if (spec == null) {
211 spec = new PKCS8EncodedKeySpec(bytes);
212 }
213
214 try {
215 return KeyFactory.getInstance("RSA").generatePrivate(spec);
216 } catch (InvalidKeySpecException ex) {
217 return KeyFactory.getInstance("DSA").generatePrivate(spec);
218 }
219 } finally {
220 input.close();
221 }
222 }
223
Doug Zongkerc0581a02013-04-10 09:19:32 -0700224 /**
225 * Add the hash(es) of every file to the manifest, creating it if
226 * necessary.
227 */
228 private static Manifest addDigestsToManifest(JarFile jar, int hashes)
Koushik Dutta29706d12012-12-17 22:25:22 -0800229 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800230 Manifest input = jar.getManifest();
231 Manifest output = new Manifest();
232 Attributes main = output.getMainAttributes();
233 if (input != null) {
234 main.putAll(input.getMainAttributes());
235 } else {
236 main.putValue("Manifest-Version", "1.0");
237 main.putValue("Created-By", "1.0 (Android SignApk)");
238 }
239
Doug Zongkerc0581a02013-04-10 09:19:32 -0700240 MessageDigest md_sha1 = null;
241 MessageDigest md_sha256 = null;
242 if ((hashes & USE_SHA1) != 0) {
243 md_sha1 = MessageDigest.getInstance("SHA1");
244 }
245 if ((hashes & USE_SHA256) != 0) {
246 md_sha256 = MessageDigest.getInstance("SHA256");
247 }
248
The Android Open Source Project88b60792009-03-03 19:28:42 -0800249 byte[] buffer = new byte[4096];
250 int num;
251
252 // We sort the input entries by name, and add them to the
253 // output manifest in sorted order. We expect that the output
254 // map will be deterministic.
255
256 TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
257
258 for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
259 JarEntry entry = e.nextElement();
260 byName.put(entry.getName(), entry);
261 }
262
263 for (JarEntry entry: byName.values()) {
264 String name = entry.getName();
Doug Zongkerb14c9762012-10-15 17:10:13 -0700265 if (!entry.isDirectory() &&
266 (stripPattern == null || !stripPattern.matcher(name).matches())) {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800267 InputStream data = jar.getInputStream(entry);
268 while ((num = data.read(buffer)) > 0) {
Doug Zongkerc0581a02013-04-10 09:19:32 -0700269 if (md_sha1 != null) md_sha1.update(buffer, 0, num);
270 if (md_sha256 != null) md_sha256.update(buffer, 0, num);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800271 }
272
273 Attributes attr = null;
274 if (input != null) attr = input.getAttributes(name);
275 attr = attr != null ? new Attributes(attr) : new Attributes();
Doug Zongkerc0581a02013-04-10 09:19:32 -0700276 if (md_sha1 != null) {
277 attr.putValue("SHA1-Digest",
278 new String(Base64.encode(md_sha1.digest()), "ASCII"));
279 }
280 if (md_sha256 != null) {
281 attr.putValue("SHA-256-Digest",
282 new String(Base64.encode(md_sha256.digest()), "ASCII"));
283 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800284 output.getEntries().put(name, attr);
285 }
286 }
287
288 return output;
289 }
290
Doug Zongker7bb04232012-05-11 09:20:50 -0700291 /**
292 * Add a copy of the public key to the archive; this should
293 * exactly match one of the files in
294 * /system/etc/security/otacerts.zip on the device. (The same
295 * cert can be extracted from the CERT.RSA file but this is much
296 * easier to get at.)
297 */
298 private static void addOtacert(JarOutputStream outputJar,
299 File publicKeyFile,
300 long timestamp,
Doug Zongkerc0581a02013-04-10 09:19:32 -0700301 Manifest manifest,
302 int hash)
Doug Zongker7bb04232012-05-11 09:20:50 -0700303 throws IOException, GeneralSecurityException {
Doug Zongkerc0581a02013-04-10 09:19:32 -0700304 MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256");
Doug Zongker7bb04232012-05-11 09:20:50 -0700305
306 JarEntry je = new JarEntry(OTACERT_NAME);
307 je.setTime(timestamp);
308 outputJar.putNextEntry(je);
309 FileInputStream input = new FileInputStream(publicKeyFile);
310 byte[] b = new byte[4096];
311 int read;
312 while ((read = input.read(b)) != -1) {
313 outputJar.write(b, 0, read);
314 md.update(b, 0, read);
315 }
316 input.close();
317
318 Attributes attr = new Attributes();
Doug Zongkerc0581a02013-04-10 09:19:32 -0700319 attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest",
Doug Zongker147626e2012-09-04 13:32:13 -0700320 new String(Base64.encode(md.digest()), "ASCII"));
Doug Zongker7bb04232012-05-11 09:20:50 -0700321 manifest.getEntries().put(OTACERT_NAME, attr);
322 }
323
324
Doug Zongker147626e2012-09-04 13:32:13 -0700325 /** Write to another stream and track how many bytes have been
326 * written.
327 */
328 private static class CountOutputStream extends FilterOutputStream {
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700329 private int mCount;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800330
Doug Zongker147626e2012-09-04 13:32:13 -0700331 public CountOutputStream(OutputStream out) {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800332 super(out);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700333 mCount = 0;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800334 }
335
336 @Override
337 public void write(int b) throws IOException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800338 super.write(b);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700339 mCount++;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800340 }
341
342 @Override
343 public void write(byte[] b, int off, int len) throws IOException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800344 super.write(b, off, len);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700345 mCount += len;
346 }
347
348 public int size() {
349 return mCount;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800350 }
351 }
352
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700353 /** Write a .SF file with a digest of the specified manifest. */
Doug Zongkerc0581a02013-04-10 09:19:32 -0700354 private static void writeSignatureFile(Manifest manifest, OutputStream out,
355 int hash)
Doug Zongker147626e2012-09-04 13:32:13 -0700356 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800357 Manifest sf = new Manifest();
358 Attributes main = sf.getMainAttributes();
359 main.putValue("Signature-Version", "1.0");
360 main.putValue("Created-By", "1.0 (Android SignApk)");
361
Doug Zongkerc0581a02013-04-10 09:19:32 -0700362 MessageDigest md = MessageDigest.getInstance(
363 hash == USE_SHA256 ? "SHA256" : "SHA1");
The Android Open Source Project88b60792009-03-03 19:28:42 -0800364 PrintStream print = new PrintStream(
Koushik Dutta29706d12012-12-17 22:25:22 -0800365 new DigestOutputStream(new ByteArrayOutputStream(), md),
366 true, "UTF-8");
The Android Open Source Project88b60792009-03-03 19:28:42 -0800367
368 // Digest of the entire manifest
369 manifest.write(print);
370 print.flush();
Doug Zongkerc0581a02013-04-10 09:19:32 -0700371 main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
Doug Zongker147626e2012-09-04 13:32:13 -0700372 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800373
374 Map<String, Attributes> entries = manifest.getEntries();
375 for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
376 // Digest of the manifest stanza for this entry.
377 print.print("Name: " + entry.getKey() + "\r\n");
378 for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
379 print.print(att.getKey() + ": " + att.getValue() + "\r\n");
380 }
381 print.print("\r\n");
382 print.flush();
383
384 Attributes sfAttr = new Attributes();
Doug Zongkerc0581a02013-04-10 09:19:32 -0700385 sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest",
Doug Zongker147626e2012-09-04 13:32:13 -0700386 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800387 sf.getEntries().put(entry.getKey(), sfAttr);
388 }
389
Doug Zongker147626e2012-09-04 13:32:13 -0700390 CountOutputStream cout = new CountOutputStream(out);
391 sf.write(cout);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700392
393 // A bug in the java.util.jar implementation of Android platforms
394 // up to version 1.6 will cause a spurious IOException to be thrown
395 // if the length of the signature file is a multiple of 1024 bytes.
396 // As a workaround, add an extra CRLF in this case.
Doug Zongker147626e2012-09-04 13:32:13 -0700397 if ((cout.size() % 1024) == 0) {
398 cout.write('\r');
399 cout.write('\n');
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700400 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800401 }
402
Doug Zongker147626e2012-09-04 13:32:13 -0700403 /** Sign data and write the digital signature to 'out'. */
The Android Open Source Project88b60792009-03-03 19:28:42 -0800404 private static void writeSignatureBlock(
Doug Zongker147626e2012-09-04 13:32:13 -0700405 CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
406 OutputStream out)
407 throws IOException,
408 CertificateEncodingException,
409 OperatorCreationException,
410 CMSException {
411 ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
412 certList.add(publicKey);
413 JcaCertStore certs = new JcaCertStore(certList);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800414
Doug Zongker147626e2012-09-04 13:32:13 -0700415 CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
Doug Zongkerc0581a02013-04-10 09:19:32 -0700416 ContentSigner signer = new JcaContentSignerBuilder(
417 getAlgorithm(publicKey) == USE_SHA256 ? "SHA256withRSA" : "SHA1withRSA")
Doug Zongker147626e2012-09-04 13:32:13 -0700418 .setProvider(sBouncyCastleProvider)
419 .build(privateKey);
420 gen.addSignerInfoGenerator(
421 new JcaSignerInfoGeneratorBuilder(
422 new JcaDigestCalculatorProviderBuilder()
423 .setProvider(sBouncyCastleProvider)
424 .build())
425 .setDirectSignature(true)
Doug Zongkerc0581a02013-04-10 09:19:32 -0700426 .build(signer, publicKey));
Doug Zongker147626e2012-09-04 13:32:13 -0700427 gen.addCertificates(certs);
428 CMSSignedData sigData = gen.generate(data, false);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800429
Doug Zongker147626e2012-09-04 13:32:13 -0700430 ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
431 DEROutputStream dos = new DEROutputStream(out);
432 dos.writeObject(asn1.readObject());
The Android Open Source Project88b60792009-03-03 19:28:42 -0800433 }
434
Koushik Dutta29706d12012-12-17 22:25:22 -0800435 /**
436 * Copy all the files in a manifest from input to output. We set
437 * the modification times in the output to a fixed time, so as to
438 * reduce variation in the output file and make incremental OTAs
439 * more efficient.
440 */
441 private static void copyFiles(Manifest manifest,
442 JarFile in, JarOutputStream out, long timestamp) throws IOException {
443 byte[] buffer = new byte[4096];
444 int num;
445
446 Map<String, Attributes> entries = manifest.getEntries();
447 ArrayList<String> names = new ArrayList<String>(entries.keySet());
448 Collections.sort(names);
449 for (String name : names) {
450 JarEntry inEntry = in.getJarEntry(name);
451 JarEntry outEntry = null;
452 if (inEntry.getMethod() == JarEntry.STORED) {
453 // Preserve the STORED method of the input entry.
454 outEntry = new JarEntry(inEntry);
455 } else {
456 // Create a new entry so that the compressed len is recomputed.
457 outEntry = new JarEntry(name);
458 }
459 outEntry.setTime(timestamp);
460 out.putNextEntry(outEntry);
461
462 InputStream data = in.getInputStream(inEntry);
463 while ((num = data.read(buffer)) > 0) {
464 out.write(buffer, 0, num);
465 }
466 out.flush();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700467 }
Koushik Dutta29706d12012-12-17 22:25:22 -0800468 }
469
470 private static class WholeFileSignerOutputStream extends FilterOutputStream {
471 private boolean closing = false;
472 private ByteArrayOutputStream footer = new ByteArrayOutputStream();
473 private OutputStream tee;
474
475 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
476 super(out);
477 this.tee = tee;
478 }
479
480 public void notifyClosing() {
481 closing = true;
482 }
483
484 public void finish() throws IOException {
485 closing = false;
486
487 byte[] data = footer.toByteArray();
488 if (data.length < 2)
489 throw new IOException("Less than two bytes written to footer");
490 write(data, 0, data.length - 2);
491 }
492
493 public byte[] getTail() {
494 return footer.toByteArray();
495 }
496
497 @Override
498 public void write(byte[] b) throws IOException {
499 write(b, 0, b.length);
500 }
501
502 @Override
503 public void write(byte[] b, int off, int len) throws IOException {
504 if (closing) {
505 // if the jar is about to close, save the footer that will be written
506 footer.write(b, off, len);
507 }
508 else {
509 // write to both output streams. out is the CMSTypedData signer and tee is the file.
510 out.write(b, off, len);
511 tee.write(b, off, len);
512 }
513 }
514
515 @Override
516 public void write(int b) throws IOException {
517 if (closing) {
518 // if the jar is about to close, save the footer that will be written
519 footer.write(b);
520 }
521 else {
522 // write to both output streams. out is the CMSTypedData signer and tee is the file.
523 out.write(b);
524 tee.write(b);
525 }
526 }
527 }
528
529 private static class CMSSigner implements CMSTypedData {
530 private JarFile inputJar;
531 private File publicKeyFile;
532 private X509Certificate publicKey;
533 private PrivateKey privateKey;
534 private String outputFile;
535 private OutputStream outputStream;
536 private final ASN1ObjectIdentifier type;
537 private WholeFileSignerOutputStream signer;
538
539 public CMSSigner(JarFile inputJar, File publicKeyFile,
540 X509Certificate publicKey, PrivateKey privateKey,
541 OutputStream outputStream) {
542 this.inputJar = inputJar;
543 this.publicKeyFile = publicKeyFile;
544 this.publicKey = publicKey;
545 this.privateKey = privateKey;
546 this.outputStream = outputStream;
547 this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
548 }
549
550 public Object getContent() {
551 throw new UnsupportedOperationException();
552 }
553
554 public ASN1ObjectIdentifier getContentType() {
555 return type;
556 }
557
558 public void write(OutputStream out) throws IOException {
559 try {
560 signer = new WholeFileSignerOutputStream(out, outputStream);
561 JarOutputStream outputJar = new JarOutputStream(signer);
562
Doug Zongkerc0581a02013-04-10 09:19:32 -0700563 int hash = getAlgorithm(publicKey);
564
565 // Assume the certificate is valid for at least an hour.
566 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
567
568 Manifest manifest = addDigestsToManifest(inputJar, hash);
569 copyFiles(manifest, inputJar, outputJar, timestamp);
570 addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
571
Koushik Dutta29706d12012-12-17 22:25:22 -0800572 signFile(manifest, inputJar,
573 new X509Certificate[]{ publicKey },
574 new PrivateKey[]{ privateKey },
575 outputJar);
Koushik Dutta29706d12012-12-17 22:25:22 -0800576
577 signer.notifyClosing();
578 outputJar.close();
579 signer.finish();
580 }
581 catch (Exception e) {
582 throw new IOException(e);
583 }
584 }
585
586 public void writeSignatureBlock(ByteArrayOutputStream temp)
587 throws IOException,
588 CertificateEncodingException,
589 OperatorCreationException,
590 CMSException {
591 SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
592 }
593
594 public WholeFileSignerOutputStream getSigner() {
595 return signer;
596 }
597 }
598
599 private static void signWholeFile(JarFile inputJar, File publicKeyFile,
600 X509Certificate publicKey, PrivateKey privateKey,
601 OutputStream outputStream) throws Exception {
602 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
603 publicKey, privateKey, outputStream);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700604
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700605 ByteArrayOutputStream temp = new ByteArrayOutputStream();
606
607 // put a readable message and a null char at the start of the
608 // archive comment, so that tools that display the comment
609 // (hopefully) show something sensible.
610 // TODO: anything more useful we can put in this message?
611 byte[] message = "signed by SignApk".getBytes("UTF-8");
612 temp.write(message);
613 temp.write(0);
Doug Zongker147626e2012-09-04 13:32:13 -0700614
Koushik Dutta29706d12012-12-17 22:25:22 -0800615 cmsOut.writeSignatureBlock(temp);
616
617 byte[] zipData = cmsOut.getSigner().getTail();
618
619 // For a zip with no archive comment, the
620 // end-of-central-directory record will be 22 bytes long, so
621 // we expect to find the EOCD marker 22 bytes from the end.
622 if (zipData[zipData.length-22] != 0x50 ||
623 zipData[zipData.length-21] != 0x4b ||
624 zipData[zipData.length-20] != 0x05 ||
625 zipData[zipData.length-19] != 0x06) {
626 throw new IllegalArgumentException("zip data already has an archive comment");
627 }
628
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700629 int total_size = temp.size() + 6;
630 if (total_size > 0xffff) {
631 throw new IllegalArgumentException("signature is too big for ZIP file comment");
632 }
633 // signature starts this many bytes from the end of the file
634 int signature_start = total_size - message.length - 1;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700635 temp.write(signature_start & 0xff);
636 temp.write((signature_start >> 8) & 0xff);
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700637 // Why the 0xff bytes? In a zip file with no archive comment,
638 // bytes [-6:-2] of the file are the little-endian offset from
639 // the start of the file to the central directory. So for the
640 // two high bytes to be 0xff 0xff, the archive would have to
Doug Zongker147626e2012-09-04 13:32:13 -0700641 // be nearly 4GB in size. So it's unlikely that a real
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700642 // commentless archive would have 0xffs here, and lets us tell
643 // an old signed archive from a new one.
644 temp.write(0xff);
645 temp.write(0xff);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700646 temp.write(total_size & 0xff);
647 temp.write((total_size >> 8) & 0xff);
648 temp.flush();
649
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700650 // Signature verification checks that the EOCD header is the
651 // last such sequence in the file (to avoid minzip finding a
652 // fake EOCD appended after the signature in its scan). The
653 // odds of producing this sequence by chance are very low, but
654 // let's catch it here if it does.
655 byte[] b = temp.toByteArray();
656 for (int i = 0; i < b.length-3; ++i) {
657 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
658 throw new IllegalArgumentException("found spurious EOCD header at " + i);
659 }
660 }
661
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700662 outputStream.write(total_size & 0xff);
663 outputStream.write((total_size >> 8) & 0xff);
664 temp.writeTo(outputStream);
665 }
666
Koushik Dutta29706d12012-12-17 22:25:22 -0800667 private static void signFile(Manifest manifest, JarFile inputJar,
668 X509Certificate[] publicKey, PrivateKey[] privateKey,
669 JarOutputStream outputJar)
670 throws Exception {
671 // Assume the certificate is valid for at least an hour.
672 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800673
Koushik Dutta29706d12012-12-17 22:25:22 -0800674 // MANIFEST.MF
Doug Zongkerc0581a02013-04-10 09:19:32 -0700675 JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
Koushik Dutta29706d12012-12-17 22:25:22 -0800676 je.setTime(timestamp);
677 outputJar.putNextEntry(je);
678 manifest.write(outputJar);
679
680 int numKeys = publicKey.length;
681 for (int k = 0; k < numKeys; ++k) {
682 // CERT.SF / CERT#.SF
683 je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
684 (String.format(CERT_SF_MULTI_NAME, k)));
685 je.setTime(timestamp);
686 outputJar.putNextEntry(je);
687 ByteArrayOutputStream baos = new ByteArrayOutputStream();
Doug Zongkerc0581a02013-04-10 09:19:32 -0700688 writeSignatureFile(manifest, baos, getAlgorithm(publicKey[k]));
Koushik Dutta29706d12012-12-17 22:25:22 -0800689 byte[] signedData = baos.toByteArray();
690 outputJar.write(signedData);
691
692 // CERT.RSA / CERT#.RSA
693 je = new JarEntry(numKeys == 1 ? CERT_RSA_NAME :
694 (String.format(CERT_RSA_MULTI_NAME, k)));
695 je.setTime(timestamp);
696 outputJar.putNextEntry(je);
697 writeSignatureBlock(new CMSProcessableByteArray(signedData),
698 publicKey[k], privateKey[k], outputJar);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800699 }
700 }
701
Doug Zongkerb14c9762012-10-15 17:10:13 -0700702 private static void usage() {
703 System.err.println("Usage: signapk [-w] " +
704 "publickey.x509[.pem] privatekey.pk8 " +
705 "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
706 "input.jar output.jar");
707 System.exit(2);
708 }
709
The Android Open Source Project88b60792009-03-03 19:28:42 -0800710 public static void main(String[] args) {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700711 if (args.length < 4) usage();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800712
Doug Zongker147626e2012-09-04 13:32:13 -0700713 sBouncyCastleProvider = new BouncyCastleProvider();
714 Security.addProvider(sBouncyCastleProvider);
715
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700716 boolean signWholeFile = false;
717 int argstart = 0;
718 if (args[0].equals("-w")) {
719 signWholeFile = true;
720 argstart = 1;
721 }
722
Doug Zongkerb14c9762012-10-15 17:10:13 -0700723 if ((args.length - argstart) % 2 == 1) usage();
724 int numKeys = ((args.length - argstart) / 2) - 1;
725 if (signWholeFile && numKeys > 1) {
726 System.err.println("Only one key may be used with -w.");
727 System.exit(2);
728 }
729
730 String inputFilename = args[args.length-2];
731 String outputFilename = args[args.length-1];
732
The Android Open Source Project88b60792009-03-03 19:28:42 -0800733 JarFile inputJar = null;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700734 FileOutputStream outputFile = null;
Doug Zongkerc0581a02013-04-10 09:19:32 -0700735 int hashes = 0;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800736
737 try {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700738 File firstPublicKeyFile = new File(args[argstart+0]);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800739
Doug Zongkerb14c9762012-10-15 17:10:13 -0700740 X509Certificate[] publicKey = new X509Certificate[numKeys];
Doug Zongkerc0581a02013-04-10 09:19:32 -0700741 try {
742 for (int i = 0; i < numKeys; ++i) {
743 int argNum = argstart + i*2;
744 publicKey[i] = readPublicKey(new File(args[argNum]));
745 hashes |= getAlgorithm(publicKey[i]);
746 }
747 } catch (IllegalArgumentException e) {
748 System.err.println(e);
749 System.exit(1);
Doug Zongkerb14c9762012-10-15 17:10:13 -0700750 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800751
Doug Zongkerb14c9762012-10-15 17:10:13 -0700752 // Set the ZIP file timestamp to the starting valid time
753 // of the 0th certificate plus one hour (to match what
754 // we've historically done).
755 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
756
757 PrivateKey[] privateKey = new PrivateKey[numKeys];
758 for (int i = 0; i < numKeys; ++i) {
759 int argNum = argstart + i*2 + 1;
760 privateKey[i] = readPrivateKey(new File(args[argNum]));
761 }
762 inputJar = new JarFile(new File(inputFilename), false); // Don't verify.
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700763
Koushik Dutta29706d12012-12-17 22:25:22 -0800764 outputFile = new FileOutputStream(outputFilename);
765
766
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700767 if (signWholeFile) {
Koushik Dutta29706d12012-12-17 22:25:22 -0800768 SignApk.signWholeFile(inputJar, firstPublicKeyFile,
769 publicKey[0], privateKey[0], outputFile);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700770 } else {
Koushik Dutta29706d12012-12-17 22:25:22 -0800771 JarOutputStream outputJar = new JarOutputStream(outputFile);
Doug Zongkere6913732012-07-03 15:03:04 -0700772
Koushik Dutta29706d12012-12-17 22:25:22 -0800773 // For signing .apks, use the maximum compression to make
774 // them as small as possible (since they live forever on
775 // the system partition). For OTA packages, use the
776 // default compression level, which is much much faster
777 // and produces output that is only a tiny bit larger
778 // (~0.1% on full OTA packages I tested).
Doug Zongkere6913732012-07-03 15:03:04 -0700779 outputJar.setLevel(9);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800780
Doug Zongkerc0581a02013-04-10 09:19:32 -0700781 Manifest manifest = addDigestsToManifest(inputJar, hashes);
782 copyFiles(manifest, inputJar, outputJar, timestamp);
783 signFile(manifest, inputJar, publicKey, privateKey, outputJar);
Koushik Dutta29706d12012-12-17 22:25:22 -0800784 outputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700785 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800786 } catch (Exception e) {
787 e.printStackTrace();
788 System.exit(1);
789 } finally {
790 try {
791 if (inputJar != null) inputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700792 if (outputFile != null) outputFile.close();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800793 } catch (IOException e) {
794 e.printStackTrace();
795 System.exit(1);
796 }
797 }
798 }
799}