blob: a1e422cfbd577211d6f9b4cc87b4efd1bff3d60f [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;
Kenny Root3d2365c2013-09-19 12:49:36 -070055import java.security.NoSuchAlgorithmException;
The Android Open Source Project88b60792009-03-03 19:28:42 -080056import java.security.PrivateKey;
Doug Zongker147626e2012-09-04 13:32:13 -070057import java.security.Provider;
Kenny Root3d2365c2013-09-19 12:49:36 -070058import java.security.PublicKey;
Doug Zongker147626e2012-09-04 13:32:13 -070059import java.security.Security;
60import java.security.cert.CertificateEncodingException;
The Android Open Source Project88b60792009-03-03 19:28:42 -080061import java.security.cert.CertificateFactory;
62import java.security.cert.X509Certificate;
63import java.security.spec.InvalidKeySpecException;
64import java.security.spec.KeySpec;
65import 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) {
138 String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
139 String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
140 if ("RSA".equalsIgnoreCase(keyType)) {
141 if (getDigestAlgorithm(cert) == USE_SHA256) {
142 return "SHA256withRSA";
143 } else {
144 return "SHA1withRSA";
145 }
146 } else if ("EC".equalsIgnoreCase(keyType)) {
147 return "SHA256withECDSA";
148 } else {
149 throw new IllegalArgumentException("unsupported key type: " + keyType);
150 }
151 }
152
Doug Zongkeraf482b62009-06-08 10:46:55 -0700153 // Files matching this pattern are not copied to the output.
154 private static Pattern stripPattern =
Kenny Root3d2365c2013-09-19 12:49:36 -0700155 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
Doug Zongkerb14c9762012-10-15 17:10:13 -0700156 Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
Doug Zongkeraf482b62009-06-08 10:46:55 -0700157
The Android Open Source Project88b60792009-03-03 19:28:42 -0800158 private static X509Certificate readPublicKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800159 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800160 FileInputStream input = new FileInputStream(file);
161 try {
162 CertificateFactory cf = CertificateFactory.getInstance("X.509");
163 return (X509Certificate) cf.generateCertificate(input);
164 } finally {
165 input.close();
166 }
167 }
168
169 /**
170 * Reads the password from stdin and returns it as a string.
171 *
172 * @param keyFile The file containing the private key. Used to prompt the user.
173 */
174 private static String readPassword(File keyFile) {
175 // TODO: use Console.readPassword() when it's available.
176 System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
177 System.out.flush();
178 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
179 try {
180 return stdin.readLine();
181 } catch (IOException ex) {
182 return null;
183 }
184 }
185
186 /**
187 * Decrypt an encrypted PKCS 8 format private key.
188 *
189 * Based on ghstark's post on Aug 6, 2006 at
190 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
191 *
192 * @param encryptedPrivateKey The raw data of the private key
193 * @param keyFile The file containing the private key
194 */
195 private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
Koushik Dutta29706d12012-12-17 22:25:22 -0800196 throws GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800197 EncryptedPrivateKeyInfo epkInfo;
198 try {
199 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
200 } catch (IOException ex) {
201 // Probably not an encrypted key.
202 return null;
203 }
204
205 char[] password = readPassword(keyFile).toCharArray();
206
207 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
208 Key key = skFactory.generateSecret(new PBEKeySpec(password));
209
210 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
211 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
212
213 try {
214 return epkInfo.getKeySpec(cipher);
215 } catch (InvalidKeySpecException ex) {
216 System.err.println("signapk: Password for " + keyFile + " may be bad.");
217 throw ex;
218 }
219 }
220
221 /** Read a PKCS 8 format private key. */
222 private static PrivateKey readPrivateKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800223 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800224 DataInputStream input = new DataInputStream(new FileInputStream(file));
225 try {
226 byte[] bytes = new byte[(int) file.length()];
227 input.read(bytes);
228
229 KeySpec spec = decryptPrivateKey(bytes, file);
230 if (spec == null) {
231 spec = new PKCS8EncodedKeySpec(bytes);
232 }
233
Kenny Root3d2365c2013-09-19 12:49:36 -0700234 PrivateKey key;
235 key = decodeAsKeyType(spec, "RSA");
236 if (key != null) {
237 return key;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800238 }
Kenny Root3d2365c2013-09-19 12:49:36 -0700239
240 key = decodeAsKeyType(spec, "EC");
241 if (key != null) {
242 return key;
243 }
244
245 throw new NoSuchAlgorithmException("Must be an EC or RSA key");
The Android Open Source Project88b60792009-03-03 19:28:42 -0800246 } finally {
247 input.close();
248 }
249 }
250
Kenny Root3d2365c2013-09-19 12:49:36 -0700251 private static PrivateKey decodeAsKeyType(KeySpec spec, String keyType)
252 throws GeneralSecurityException {
253 try {
254 return KeyFactory.getInstance(keyType).generatePrivate(spec);
255 } catch (InvalidKeySpecException e) {
256 return null;
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 .setProvider(sBouncyCastleProvider)
454 .build(privateKey);
455 gen.addSignerInfoGenerator(
456 new JcaSignerInfoGeneratorBuilder(
457 new JcaDigestCalculatorProviderBuilder()
458 .setProvider(sBouncyCastleProvider)
459 .build())
460 .setDirectSignature(true)
Doug Zongker8562fd42013-04-10 09:19:32 -0700461 .build(signer, publicKey));
Doug Zongker147626e2012-09-04 13:32:13 -0700462 gen.addCertificates(certs);
463 CMSSignedData sigData = gen.generate(data, false);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800464
Doug Zongker147626e2012-09-04 13:32:13 -0700465 ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
466 DEROutputStream dos = new DEROutputStream(out);
467 dos.writeObject(asn1.readObject());
The Android Open Source Project88b60792009-03-03 19:28:42 -0800468 }
469
Koushik Dutta29706d12012-12-17 22:25:22 -0800470 /**
471 * Copy all the files in a manifest from input to output. We set
472 * the modification times in the output to a fixed time, so as to
473 * reduce variation in the output file and make incremental OTAs
474 * more efficient.
475 */
476 private static void copyFiles(Manifest manifest,
477 JarFile in, JarOutputStream out, long timestamp) throws IOException {
478 byte[] buffer = new byte[4096];
479 int num;
480
481 Map<String, Attributes> entries = manifest.getEntries();
482 ArrayList<String> names = new ArrayList<String>(entries.keySet());
483 Collections.sort(names);
484 for (String name : names) {
485 JarEntry inEntry = in.getJarEntry(name);
486 JarEntry outEntry = null;
487 if (inEntry.getMethod() == JarEntry.STORED) {
488 // Preserve the STORED method of the input entry.
489 outEntry = new JarEntry(inEntry);
490 } else {
491 // Create a new entry so that the compressed len is recomputed.
492 outEntry = new JarEntry(name);
493 }
494 outEntry.setTime(timestamp);
495 out.putNextEntry(outEntry);
496
497 InputStream data = in.getInputStream(inEntry);
498 while ((num = data.read(buffer)) > 0) {
499 out.write(buffer, 0, num);
500 }
501 out.flush();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700502 }
Koushik Dutta29706d12012-12-17 22:25:22 -0800503 }
504
505 private static class WholeFileSignerOutputStream extends FilterOutputStream {
506 private boolean closing = false;
507 private ByteArrayOutputStream footer = new ByteArrayOutputStream();
508 private OutputStream tee;
509
510 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
511 super(out);
512 this.tee = tee;
513 }
514
515 public void notifyClosing() {
516 closing = true;
517 }
518
519 public void finish() throws IOException {
520 closing = false;
521
522 byte[] data = footer.toByteArray();
523 if (data.length < 2)
524 throw new IOException("Less than two bytes written to footer");
525 write(data, 0, data.length - 2);
526 }
527
528 public byte[] getTail() {
529 return footer.toByteArray();
530 }
531
532 @Override
533 public void write(byte[] b) throws IOException {
534 write(b, 0, b.length);
535 }
536
537 @Override
538 public void write(byte[] b, int off, int len) throws IOException {
539 if (closing) {
540 // if the jar is about to close, save the footer that will be written
541 footer.write(b, off, len);
542 }
543 else {
544 // write to both output streams. out is the CMSTypedData signer and tee is the file.
545 out.write(b, off, len);
546 tee.write(b, off, len);
547 }
548 }
549
550 @Override
551 public void write(int b) throws IOException {
552 if (closing) {
553 // if the jar is about to close, save the footer that will be written
554 footer.write(b);
555 }
556 else {
557 // write to both output streams. out is the CMSTypedData signer and tee is the file.
558 out.write(b);
559 tee.write(b);
560 }
561 }
562 }
563
564 private static class CMSSigner implements CMSTypedData {
565 private JarFile inputJar;
566 private File publicKeyFile;
567 private X509Certificate publicKey;
568 private PrivateKey privateKey;
569 private String outputFile;
570 private OutputStream outputStream;
571 private final ASN1ObjectIdentifier type;
572 private WholeFileSignerOutputStream signer;
573
574 public CMSSigner(JarFile inputJar, File publicKeyFile,
575 X509Certificate publicKey, PrivateKey privateKey,
576 OutputStream outputStream) {
577 this.inputJar = inputJar;
578 this.publicKeyFile = publicKeyFile;
579 this.publicKey = publicKey;
580 this.privateKey = privateKey;
581 this.outputStream = outputStream;
582 this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
583 }
584
585 public Object getContent() {
586 throw new UnsupportedOperationException();
587 }
588
589 public ASN1ObjectIdentifier getContentType() {
590 return type;
591 }
592
593 public void write(OutputStream out) throws IOException {
594 try {
595 signer = new WholeFileSignerOutputStream(out, outputStream);
596 JarOutputStream outputJar = new JarOutputStream(signer);
597
Kenny Root3d2365c2013-09-19 12:49:36 -0700598 int hash = getDigestAlgorithm(publicKey);
Doug Zongker8562fd42013-04-10 09:19:32 -0700599
600 // Assume the certificate is valid for at least an hour.
601 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
602
603 Manifest manifest = addDigestsToManifest(inputJar, hash);
604 copyFiles(manifest, inputJar, outputJar, timestamp);
605 addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
606
Koushik Dutta29706d12012-12-17 22:25:22 -0800607 signFile(manifest, inputJar,
608 new X509Certificate[]{ publicKey },
609 new PrivateKey[]{ privateKey },
610 outputJar);
Koushik Dutta29706d12012-12-17 22:25:22 -0800611
612 signer.notifyClosing();
613 outputJar.close();
614 signer.finish();
615 }
616 catch (Exception e) {
617 throw new IOException(e);
618 }
619 }
620
621 public void writeSignatureBlock(ByteArrayOutputStream temp)
622 throws IOException,
623 CertificateEncodingException,
624 OperatorCreationException,
625 CMSException {
626 SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
627 }
628
629 public WholeFileSignerOutputStream getSigner() {
630 return signer;
631 }
632 }
633
634 private static void signWholeFile(JarFile inputJar, File publicKeyFile,
635 X509Certificate publicKey, PrivateKey privateKey,
636 OutputStream outputStream) throws Exception {
637 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
638 publicKey, privateKey, outputStream);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700639
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700640 ByteArrayOutputStream temp = new ByteArrayOutputStream();
641
642 // put a readable message and a null char at the start of the
643 // archive comment, so that tools that display the comment
644 // (hopefully) show something sensible.
645 // TODO: anything more useful we can put in this message?
646 byte[] message = "signed by SignApk".getBytes("UTF-8");
647 temp.write(message);
648 temp.write(0);
Doug Zongker147626e2012-09-04 13:32:13 -0700649
Koushik Dutta29706d12012-12-17 22:25:22 -0800650 cmsOut.writeSignatureBlock(temp);
651
652 byte[] zipData = cmsOut.getSigner().getTail();
653
654 // For a zip with no archive comment, the
655 // end-of-central-directory record will be 22 bytes long, so
656 // we expect to find the EOCD marker 22 bytes from the end.
657 if (zipData[zipData.length-22] != 0x50 ||
658 zipData[zipData.length-21] != 0x4b ||
659 zipData[zipData.length-20] != 0x05 ||
660 zipData[zipData.length-19] != 0x06) {
661 throw new IllegalArgumentException("zip data already has an archive comment");
662 }
663
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700664 int total_size = temp.size() + 6;
665 if (total_size > 0xffff) {
666 throw new IllegalArgumentException("signature is too big for ZIP file comment");
667 }
668 // signature starts this many bytes from the end of the file
669 int signature_start = total_size - message.length - 1;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700670 temp.write(signature_start & 0xff);
671 temp.write((signature_start >> 8) & 0xff);
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700672 // Why the 0xff bytes? In a zip file with no archive comment,
673 // bytes [-6:-2] of the file are the little-endian offset from
674 // the start of the file to the central directory. So for the
675 // two high bytes to be 0xff 0xff, the archive would have to
Doug Zongker147626e2012-09-04 13:32:13 -0700676 // be nearly 4GB in size. So it's unlikely that a real
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700677 // commentless archive would have 0xffs here, and lets us tell
678 // an old signed archive from a new one.
679 temp.write(0xff);
680 temp.write(0xff);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700681 temp.write(total_size & 0xff);
682 temp.write((total_size >> 8) & 0xff);
683 temp.flush();
684
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700685 // Signature verification checks that the EOCD header is the
686 // last such sequence in the file (to avoid minzip finding a
687 // fake EOCD appended after the signature in its scan). The
688 // odds of producing this sequence by chance are very low, but
689 // let's catch it here if it does.
690 byte[] b = temp.toByteArray();
691 for (int i = 0; i < b.length-3; ++i) {
692 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
693 throw new IllegalArgumentException("found spurious EOCD header at " + i);
694 }
695 }
696
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700697 outputStream.write(total_size & 0xff);
698 outputStream.write((total_size >> 8) & 0xff);
699 temp.writeTo(outputStream);
700 }
701
Koushik Dutta29706d12012-12-17 22:25:22 -0800702 private static void signFile(Manifest manifest, JarFile inputJar,
703 X509Certificate[] publicKey, PrivateKey[] privateKey,
704 JarOutputStream outputJar)
705 throws Exception {
706 // Assume the certificate is valid for at least an hour.
707 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800708
Koushik Dutta29706d12012-12-17 22:25:22 -0800709 // MANIFEST.MF
Doug Zongker8562fd42013-04-10 09:19:32 -0700710 JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
Koushik Dutta29706d12012-12-17 22:25:22 -0800711 je.setTime(timestamp);
712 outputJar.putNextEntry(je);
713 manifest.write(outputJar);
714
715 int numKeys = publicKey.length;
716 for (int k = 0; k < numKeys; ++k) {
717 // CERT.SF / CERT#.SF
718 je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
719 (String.format(CERT_SF_MULTI_NAME, k)));
720 je.setTime(timestamp);
721 outputJar.putNextEntry(je);
722 ByteArrayOutputStream baos = new ByteArrayOutputStream();
Kenny Root3d2365c2013-09-19 12:49:36 -0700723 writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
Koushik Dutta29706d12012-12-17 22:25:22 -0800724 byte[] signedData = baos.toByteArray();
725 outputJar.write(signedData);
726
Kenny Root3d2365c2013-09-19 12:49:36 -0700727 // CERT.{EC,RSA} / CERT#.{EC,RSA}
728 je = new JarEntry(numKeys == 1 ?
729 (String.format(CERT_SIG_NAME, privateKey[k].getAlgorithm())) :
730 (String.format(CERT_SIG_MULTI_NAME, k, privateKey[k].getAlgorithm())));
Koushik Dutta29706d12012-12-17 22:25:22 -0800731 je.setTime(timestamp);
732 outputJar.putNextEntry(je);
733 writeSignatureBlock(new CMSProcessableByteArray(signedData),
734 publicKey[k], privateKey[k], outputJar);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800735 }
736 }
737
Doug Zongkerb14c9762012-10-15 17:10:13 -0700738 private static void usage() {
739 System.err.println("Usage: signapk [-w] " +
740 "publickey.x509[.pem] privatekey.pk8 " +
741 "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
742 "input.jar output.jar");
743 System.exit(2);
744 }
745
The Android Open Source Project88b60792009-03-03 19:28:42 -0800746 public static void main(String[] args) {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700747 if (args.length < 4) usage();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800748
Doug Zongker147626e2012-09-04 13:32:13 -0700749 sBouncyCastleProvider = new BouncyCastleProvider();
750 Security.addProvider(sBouncyCastleProvider);
751
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700752 boolean signWholeFile = false;
753 int argstart = 0;
754 if (args[0].equals("-w")) {
755 signWholeFile = true;
756 argstart = 1;
757 }
758
Doug Zongkerb14c9762012-10-15 17:10:13 -0700759 if ((args.length - argstart) % 2 == 1) usage();
760 int numKeys = ((args.length - argstart) / 2) - 1;
761 if (signWholeFile && numKeys > 1) {
762 System.err.println("Only one key may be used with -w.");
763 System.exit(2);
764 }
765
766 String inputFilename = args[args.length-2];
767 String outputFilename = args[args.length-1];
768
The Android Open Source Project88b60792009-03-03 19:28:42 -0800769 JarFile inputJar = null;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700770 FileOutputStream outputFile = null;
Doug Zongker8562fd42013-04-10 09:19:32 -0700771 int hashes = 0;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800772
773 try {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700774 File firstPublicKeyFile = new File(args[argstart+0]);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800775
Doug Zongkerb14c9762012-10-15 17:10:13 -0700776 X509Certificate[] publicKey = new X509Certificate[numKeys];
Doug Zongker8562fd42013-04-10 09:19:32 -0700777 try {
778 for (int i = 0; i < numKeys; ++i) {
779 int argNum = argstart + i*2;
780 publicKey[i] = readPublicKey(new File(args[argNum]));
Kenny Root3d2365c2013-09-19 12:49:36 -0700781 hashes |= getDigestAlgorithm(publicKey[i]);
Doug Zongker8562fd42013-04-10 09:19:32 -0700782 }
783 } catch (IllegalArgumentException e) {
784 System.err.println(e);
785 System.exit(1);
Doug Zongkerb14c9762012-10-15 17:10:13 -0700786 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800787
Doug Zongkerb14c9762012-10-15 17:10:13 -0700788 // Set the ZIP file timestamp to the starting valid time
789 // of the 0th certificate plus one hour (to match what
790 // we've historically done).
791 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
792
793 PrivateKey[] privateKey = new PrivateKey[numKeys];
794 for (int i = 0; i < numKeys; ++i) {
795 int argNum = argstart + i*2 + 1;
796 privateKey[i] = readPrivateKey(new File(args[argNum]));
797 }
798 inputJar = new JarFile(new File(inputFilename), false); // Don't verify.
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700799
Koushik Dutta29706d12012-12-17 22:25:22 -0800800 outputFile = new FileOutputStream(outputFilename);
801
802
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700803 if (signWholeFile) {
Kenny Root3d2365c2013-09-19 12:49:36 -0700804 if (!"RSA".equalsIgnoreCase(privateKey[0].getAlgorithm())) {
805 System.err.println("Cannot sign OTA packages with non-RSA keys");
806 System.exit(1);
807 }
Koushik Dutta29706d12012-12-17 22:25:22 -0800808 SignApk.signWholeFile(inputJar, firstPublicKeyFile,
809 publicKey[0], privateKey[0], outputFile);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700810 } else {
Koushik Dutta29706d12012-12-17 22:25:22 -0800811 JarOutputStream outputJar = new JarOutputStream(outputFile);
Doug Zongkere6913732012-07-03 15:03:04 -0700812
Koushik Dutta29706d12012-12-17 22:25:22 -0800813 // For signing .apks, use the maximum compression to make
814 // them as small as possible (since they live forever on
815 // the system partition). For OTA packages, use the
816 // default compression level, which is much much faster
817 // and produces output that is only a tiny bit larger
818 // (~0.1% on full OTA packages I tested).
Doug Zongkere6913732012-07-03 15:03:04 -0700819 outputJar.setLevel(9);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800820
Doug Zongker8562fd42013-04-10 09:19:32 -0700821 Manifest manifest = addDigestsToManifest(inputJar, hashes);
822 copyFiles(manifest, inputJar, outputJar, timestamp);
823 signFile(manifest, inputJar, publicKey, privateKey, outputJar);
Koushik Dutta29706d12012-12-17 22:25:22 -0800824 outputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700825 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800826 } catch (Exception e) {
827 e.printStackTrace();
828 System.exit(1);
829 } finally {
830 try {
831 if (inputJar != null) inputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700832 if (outputFile != null) outputFile.close();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800833 } catch (IOException e) {
834 e.printStackTrace();
835 System.exit(1);
836 }
837 }
838 }
839}