blob: d4e49e6ee9f59afd0cd846f5d4a0a57766eeee0f [file] [log] [blame]
The Android Open Source Project88b60792009-03-03 19:28:42 -08001/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.signapk;
18
Doug Zongker147626e2012-09-04 13:32:13 -070019import org.bouncycastle.asn1.ASN1InputStream;
20import org.bouncycastle.asn1.ASN1ObjectIdentifier;
21import org.bouncycastle.asn1.DEROutputStream;
22import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
Kenny Root62ea4a52013-09-25 09:59:10 -070023import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
Doug Zongker147626e2012-09-04 13:32:13 -070024import org.bouncycastle.cert.jcajce.JcaCertStore;
25import org.bouncycastle.cms.CMSException;
26import org.bouncycastle.cms.CMSProcessableByteArray;
27import org.bouncycastle.cms.CMSSignedData;
28import org.bouncycastle.cms.CMSSignedDataGenerator;
29import org.bouncycastle.cms.CMSTypedData;
30import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
31import org.bouncycastle.jce.provider.BouncyCastleProvider;
32import org.bouncycastle.operator.ContentSigner;
33import org.bouncycastle.operator.OperatorCreationException;
34import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
35import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
36import org.bouncycastle.util.encoders.Base64;
The Android Open Source Project88b60792009-03-03 19:28:42 -080037
38import java.io.BufferedReader;
Kenny Root62ea4a52013-09-25 09:59:10 -070039import java.io.ByteArrayInputStream;
The Android Open Source Project88b60792009-03-03 19:28:42 -080040import java.io.ByteArrayOutputStream;
41import java.io.DataInputStream;
42import java.io.File;
43import java.io.FileInputStream;
44import java.io.FileOutputStream;
45import java.io.FilterOutputStream;
46import java.io.IOException;
47import java.io.InputStream;
48import java.io.InputStreamReader;
49import java.io.OutputStream;
50import java.io.PrintStream;
The Android Open Source Project88b60792009-03-03 19:28:42 -080051import java.security.DigestOutputStream;
52import java.security.GeneralSecurityException;
53import java.security.Key;
54import java.security.KeyFactory;
55import java.security.MessageDigest;
56import java.security.PrivateKey;
Doug Zongker147626e2012-09-04 13:32:13 -070057import java.security.Provider;
58import java.security.Security;
59import java.security.cert.CertificateEncodingException;
The Android Open Source Project88b60792009-03-03 19:28:42 -080060import java.security.cert.CertificateFactory;
61import java.security.cert.X509Certificate;
62import java.security.spec.InvalidKeySpecException;
The Android Open Source Project88b60792009-03-03 19:28:42 -080063import 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;
Kenny Root3d2365c2013-09-19 12:49:36 -070067import java.util.Locale;
The Android Open Source Project88b60792009-03-03 19:28:42 -080068import java.util.Map;
69import java.util.TreeMap;
70import java.util.jar.Attributes;
71import java.util.jar.JarEntry;
72import java.util.jar.JarFile;
73import java.util.jar.JarOutputStream;
74import java.util.jar.Manifest;
Doug Zongkeraf482b62009-06-08 10:46:55 -070075import java.util.regex.Pattern;
The Android Open Source Project88b60792009-03-03 19:28:42 -080076import javax.crypto.Cipher;
77import javax.crypto.EncryptedPrivateKeyInfo;
78import javax.crypto.SecretKeyFactory;
79import javax.crypto.spec.PBEKeySpec;
80
81/**
Doug Zongker8562fd42013-04-10 09:19:32 -070082 * HISTORICAL NOTE:
83 *
84 * Prior to the keylimepie release, SignApk ignored the signature
85 * algorithm specified in the certificate and always used SHA1withRSA.
86 *
Kenny Root3d2365c2013-09-19 12:49:36 -070087 * Starting with JB-MR2, the platform supports SHA256withRSA, so we use
88 * the signature algorithm in the certificate to select which to use
89 * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported.
Doug Zongker8562fd42013-04-10 09:19:32 -070090 *
91 * Because there are old keys still in use whose certificate actually
92 * says "MD5withRSA", we treat these as though they say "SHA1withRSA"
93 * for compatibility with older releases. This can be changed by
94 * altering the getAlgorithm() function below.
95 */
96
97
98/**
Kenny Root3d2365c2013-09-19 12:49:36 -070099 * Command line tool to sign JAR files (including APKs and OTA updates) in a way
100 * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or
101 * SHA-256 (see historical note).
The Android Open Source Project88b60792009-03-03 19:28:42 -0800102 */
103class SignApk {
104 private static final String CERT_SF_NAME = "META-INF/CERT.SF";
Kenny Root3d2365c2013-09-19 12:49:36 -0700105 private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
Doug Zongkerb14c9762012-10-15 17:10:13 -0700106 private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
Kenny Root3d2365c2013-09-19 12:49:36 -0700107 private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s";
The Android Open Source Project88b60792009-03-03 19:28:42 -0800108
Doug Zongker7bb04232012-05-11 09:20:50 -0700109 private static final String OTACERT_NAME = "META-INF/com/android/otacert";
110
Doug Zongker147626e2012-09-04 13:32:13 -0700111 private static Provider sBouncyCastleProvider;
112
Doug Zongker8562fd42013-04-10 09:19:32 -0700113 // bitmasks for which hash algorithms we need the manifest to include.
114 private static final int USE_SHA1 = 1;
115 private static final int USE_SHA256 = 2;
116
117 /**
118 * Return one of USE_SHA1 or USE_SHA256 according to the signature
119 * algorithm specified in the cert.
120 */
Kenny Root3d2365c2013-09-19 12:49:36 -0700121 private static int getDigestAlgorithm(X509Certificate cert) {
122 String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
123 if ("SHA1WITHRSA".equals(sigAlg) ||
124 "MD5WITHRSA".equals(sigAlg)) { // see "HISTORICAL NOTE" above.
Doug Zongker8562fd42013-04-10 09:19:32 -0700125 return USE_SHA1;
Kenny Root3d2365c2013-09-19 12:49:36 -0700126 } else if (sigAlg.startsWith("SHA256WITH")) {
Doug Zongker8562fd42013-04-10 09:19:32 -0700127 return USE_SHA256;
128 } else {
129 throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
130 "\" in cert [" + cert.getSubjectDN());
131 }
132 }
133
Kenny Root3d2365c2013-09-19 12:49:36 -0700134 /** Returns the expected signature algorithm for this key type. */
135 private static String getSignatureAlgorithm(X509Certificate cert) {
136 String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
137 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 /**
168 * Reads the password from stdin and returns it as a string.
169 *
170 * @param keyFile The file containing the private key. Used to prompt the user.
171 */
172 private static String readPassword(File keyFile) {
173 // TODO: use Console.readPassword() when it's available.
174 System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
175 System.out.flush();
176 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
177 try {
178 return stdin.readLine();
179 } catch (IOException ex) {
180 return null;
181 }
182 }
183
184 /**
Kenny Root62ea4a52013-09-25 09:59:10 -0700185 * Decrypt an encrypted PKCS#8 format private key.
The Android Open Source Project88b60792009-03-03 19:28:42 -0800186 *
187 * Based on ghstark's post on Aug 6, 2006 at
188 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
189 *
190 * @param encryptedPrivateKey The raw data of the private key
191 * @param keyFile The file containing the private key
192 */
Kenny Root62ea4a52013-09-25 09:59:10 -0700193 private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
Koushik Dutta29706d12012-12-17 22:25:22 -0800194 throws GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800195 EncryptedPrivateKeyInfo epkInfo;
196 try {
197 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
198 } catch (IOException ex) {
199 // Probably not an encrypted key.
200 return null;
201 }
202
203 char[] password = readPassword(keyFile).toCharArray();
204
205 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
206 Key key = skFactory.generateSecret(new PBEKeySpec(password));
207
208 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
209 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
210
211 try {
212 return epkInfo.getKeySpec(cipher);
213 } catch (InvalidKeySpecException ex) {
214 System.err.println("signapk: Password for " + keyFile + " may be bad.");
215 throw ex;
216 }
217 }
218
Kenny Root62ea4a52013-09-25 09:59:10 -0700219 /** Read a PKCS#8 format private key. */
The Android Open Source Project88b60792009-03-03 19:28:42 -0800220 private static PrivateKey readPrivateKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800221 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800222 DataInputStream input = new DataInputStream(new FileInputStream(file));
223 try {
224 byte[] bytes = new byte[(int) file.length()];
225 input.read(bytes);
226
Kenny Root62ea4a52013-09-25 09:59:10 -0700227 /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
228 PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800229 if (spec == null) {
230 spec = new PKCS8EncodedKeySpec(bytes);
231 }
232
Kenny Root62ea4a52013-09-25 09:59:10 -0700233 /*
234 * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
235 * OID and use that to construct a KeyFactory.
236 */
237 ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()));
238 PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject());
239 String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
Kenny Root3d2365c2013-09-19 12:49:36 -0700240
Kenny Root62ea4a52013-09-25 09:59:10 -0700241 return KeyFactory.getInstance(algOid).generatePrivate(spec);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800242 } finally {
243 input.close();
244 }
245 }
246
Doug Zongker8562fd42013-04-10 09:19:32 -0700247 /**
248 * Add the hash(es) of every file to the manifest, creating it if
249 * necessary.
250 */
251 private static Manifest addDigestsToManifest(JarFile jar, int hashes)
Koushik Dutta29706d12012-12-17 22:25:22 -0800252 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800253 Manifest input = jar.getManifest();
254 Manifest output = new Manifest();
255 Attributes main = output.getMainAttributes();
256 if (input != null) {
257 main.putAll(input.getMainAttributes());
258 } else {
259 main.putValue("Manifest-Version", "1.0");
260 main.putValue("Created-By", "1.0 (Android SignApk)");
261 }
262
Doug Zongker8562fd42013-04-10 09:19:32 -0700263 MessageDigest md_sha1 = null;
264 MessageDigest md_sha256 = null;
265 if ((hashes & USE_SHA1) != 0) {
266 md_sha1 = MessageDigest.getInstance("SHA1");
267 }
268 if ((hashes & USE_SHA256) != 0) {
269 md_sha256 = MessageDigest.getInstance("SHA256");
270 }
271
The Android Open Source Project88b60792009-03-03 19:28:42 -0800272 byte[] buffer = new byte[4096];
273 int num;
274
275 // We sort the input entries by name, and add them to the
276 // output manifest in sorted order. We expect that the output
277 // map will be deterministic.
278
279 TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
280
281 for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
282 JarEntry entry = e.nextElement();
283 byName.put(entry.getName(), entry);
284 }
285
286 for (JarEntry entry: byName.values()) {
287 String name = entry.getName();
Doug Zongkerb14c9762012-10-15 17:10:13 -0700288 if (!entry.isDirectory() &&
289 (stripPattern == null || !stripPattern.matcher(name).matches())) {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800290 InputStream data = jar.getInputStream(entry);
291 while ((num = data.read(buffer)) > 0) {
Doug Zongker8562fd42013-04-10 09:19:32 -0700292 if (md_sha1 != null) md_sha1.update(buffer, 0, num);
293 if (md_sha256 != null) md_sha256.update(buffer, 0, num);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800294 }
295
296 Attributes attr = null;
297 if (input != null) attr = input.getAttributes(name);
298 attr = attr != null ? new Attributes(attr) : new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700299 if (md_sha1 != null) {
300 attr.putValue("SHA1-Digest",
301 new String(Base64.encode(md_sha1.digest()), "ASCII"));
302 }
303 if (md_sha256 != null) {
304 attr.putValue("SHA-256-Digest",
305 new String(Base64.encode(md_sha256.digest()), "ASCII"));
306 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800307 output.getEntries().put(name, attr);
308 }
309 }
310
311 return output;
312 }
313
Doug Zongker7bb04232012-05-11 09:20:50 -0700314 /**
315 * Add a copy of the public key to the archive; this should
316 * exactly match one of the files in
317 * /system/etc/security/otacerts.zip on the device. (The same
318 * cert can be extracted from the CERT.RSA file but this is much
319 * easier to get at.)
320 */
321 private static void addOtacert(JarOutputStream outputJar,
322 File publicKeyFile,
323 long timestamp,
Doug Zongker8562fd42013-04-10 09:19:32 -0700324 Manifest manifest,
325 int hash)
Doug Zongker7bb04232012-05-11 09:20:50 -0700326 throws IOException, GeneralSecurityException {
Doug Zongker8562fd42013-04-10 09:19:32 -0700327 MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256");
Doug Zongker7bb04232012-05-11 09:20:50 -0700328
329 JarEntry je = new JarEntry(OTACERT_NAME);
330 je.setTime(timestamp);
331 outputJar.putNextEntry(je);
332 FileInputStream input = new FileInputStream(publicKeyFile);
333 byte[] b = new byte[4096];
334 int read;
335 while ((read = input.read(b)) != -1) {
336 outputJar.write(b, 0, read);
337 md.update(b, 0, read);
338 }
339 input.close();
340
341 Attributes attr = new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700342 attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest",
Doug Zongker147626e2012-09-04 13:32:13 -0700343 new String(Base64.encode(md.digest()), "ASCII"));
Doug Zongker7bb04232012-05-11 09:20:50 -0700344 manifest.getEntries().put(OTACERT_NAME, attr);
345 }
346
347
Doug Zongker147626e2012-09-04 13:32:13 -0700348 /** Write to another stream and track how many bytes have been
349 * written.
350 */
351 private static class CountOutputStream extends FilterOutputStream {
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700352 private int mCount;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800353
Doug Zongker147626e2012-09-04 13:32:13 -0700354 public CountOutputStream(OutputStream out) {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800355 super(out);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700356 mCount = 0;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800357 }
358
359 @Override
360 public void write(int b) throws IOException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800361 super.write(b);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700362 mCount++;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800363 }
364
365 @Override
366 public void write(byte[] b, int off, int len) throws IOException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800367 super.write(b, off, len);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700368 mCount += len;
369 }
370
371 public int size() {
372 return mCount;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800373 }
374 }
375
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700376 /** Write a .SF file with a digest of the specified manifest. */
Doug Zongker8562fd42013-04-10 09:19:32 -0700377 private static void writeSignatureFile(Manifest manifest, OutputStream out,
378 int hash)
Doug Zongker147626e2012-09-04 13:32:13 -0700379 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800380 Manifest sf = new Manifest();
381 Attributes main = sf.getMainAttributes();
382 main.putValue("Signature-Version", "1.0");
383 main.putValue("Created-By", "1.0 (Android SignApk)");
384
Doug Zongker8562fd42013-04-10 09:19:32 -0700385 MessageDigest md = MessageDigest.getInstance(
386 hash == USE_SHA256 ? "SHA256" : "SHA1");
The Android Open Source Project88b60792009-03-03 19:28:42 -0800387 PrintStream print = new PrintStream(
Koushik Dutta29706d12012-12-17 22:25:22 -0800388 new DigestOutputStream(new ByteArrayOutputStream(), md),
389 true, "UTF-8");
The Android Open Source Project88b60792009-03-03 19:28:42 -0800390
391 // Digest of the entire manifest
392 manifest.write(print);
393 print.flush();
Doug Zongker8562fd42013-04-10 09:19:32 -0700394 main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
Doug Zongker147626e2012-09-04 13:32:13 -0700395 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800396
397 Map<String, Attributes> entries = manifest.getEntries();
398 for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
399 // Digest of the manifest stanza for this entry.
400 print.print("Name: " + entry.getKey() + "\r\n");
401 for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
402 print.print(att.getKey() + ": " + att.getValue() + "\r\n");
403 }
404 print.print("\r\n");
405 print.flush();
406
407 Attributes sfAttr = new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700408 sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest",
Doug Zongker147626e2012-09-04 13:32:13 -0700409 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800410 sf.getEntries().put(entry.getKey(), sfAttr);
411 }
412
Doug Zongker147626e2012-09-04 13:32:13 -0700413 CountOutputStream cout = new CountOutputStream(out);
414 sf.write(cout);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700415
416 // A bug in the java.util.jar implementation of Android platforms
417 // up to version 1.6 will cause a spurious IOException to be thrown
418 // if the length of the signature file is a multiple of 1024 bytes.
419 // As a workaround, add an extra CRLF in this case.
Doug Zongker147626e2012-09-04 13:32:13 -0700420 if ((cout.size() % 1024) == 0) {
421 cout.write('\r');
422 cout.write('\n');
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700423 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800424 }
425
Doug Zongker147626e2012-09-04 13:32:13 -0700426 /** Sign data and write the digital signature to 'out'. */
The Android Open Source Project88b60792009-03-03 19:28:42 -0800427 private static void writeSignatureBlock(
Doug Zongker147626e2012-09-04 13:32:13 -0700428 CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
429 OutputStream out)
430 throws IOException,
431 CertificateEncodingException,
432 OperatorCreationException,
433 CMSException {
434 ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
435 certList.add(publicKey);
436 JcaCertStore certs = new JcaCertStore(certList);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800437
Doug Zongker147626e2012-09-04 13:32:13 -0700438 CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
Kenny Root3d2365c2013-09-19 12:49:36 -0700439 ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
Doug Zongker147626e2012-09-04 13:32:13 -0700440 .setProvider(sBouncyCastleProvider)
441 .build(privateKey);
442 gen.addSignerInfoGenerator(
443 new JcaSignerInfoGeneratorBuilder(
444 new JcaDigestCalculatorProviderBuilder()
445 .setProvider(sBouncyCastleProvider)
446 .build())
447 .setDirectSignature(true)
Doug Zongker8562fd42013-04-10 09:19:32 -0700448 .build(signer, publicKey));
Doug Zongker147626e2012-09-04 13:32:13 -0700449 gen.addCertificates(certs);
450 CMSSignedData sigData = gen.generate(data, false);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800451
Doug Zongker147626e2012-09-04 13:32:13 -0700452 ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
453 DEROutputStream dos = new DEROutputStream(out);
454 dos.writeObject(asn1.readObject());
The Android Open Source Project88b60792009-03-03 19:28:42 -0800455 }
456
Koushik Dutta29706d12012-12-17 22:25:22 -0800457 /**
458 * Copy all the files in a manifest from input to output. We set
459 * the modification times in the output to a fixed time, so as to
460 * reduce variation in the output file and make incremental OTAs
461 * more efficient.
462 */
463 private static void copyFiles(Manifest manifest,
464 JarFile in, JarOutputStream out, long timestamp) throws IOException {
465 byte[] buffer = new byte[4096];
466 int num;
467
468 Map<String, Attributes> entries = manifest.getEntries();
469 ArrayList<String> names = new ArrayList<String>(entries.keySet());
470 Collections.sort(names);
471 for (String name : names) {
472 JarEntry inEntry = in.getJarEntry(name);
473 JarEntry outEntry = null;
474 if (inEntry.getMethod() == JarEntry.STORED) {
475 // Preserve the STORED method of the input entry.
476 outEntry = new JarEntry(inEntry);
477 } else {
478 // Create a new entry so that the compressed len is recomputed.
479 outEntry = new JarEntry(name);
480 }
481 outEntry.setTime(timestamp);
482 out.putNextEntry(outEntry);
483
484 InputStream data = in.getInputStream(inEntry);
485 while ((num = data.read(buffer)) > 0) {
486 out.write(buffer, 0, num);
487 }
488 out.flush();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700489 }
Koushik Dutta29706d12012-12-17 22:25:22 -0800490 }
491
492 private static class WholeFileSignerOutputStream extends FilterOutputStream {
493 private boolean closing = false;
494 private ByteArrayOutputStream footer = new ByteArrayOutputStream();
495 private OutputStream tee;
496
497 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
498 super(out);
499 this.tee = tee;
500 }
501
502 public void notifyClosing() {
503 closing = true;
504 }
505
506 public void finish() throws IOException {
507 closing = false;
508
509 byte[] data = footer.toByteArray();
510 if (data.length < 2)
511 throw new IOException("Less than two bytes written to footer");
512 write(data, 0, data.length - 2);
513 }
514
515 public byte[] getTail() {
516 return footer.toByteArray();
517 }
518
519 @Override
520 public void write(byte[] b) throws IOException {
521 write(b, 0, b.length);
522 }
523
524 @Override
525 public void write(byte[] b, int off, int len) throws IOException {
526 if (closing) {
527 // if the jar is about to close, save the footer that will be written
528 footer.write(b, off, len);
529 }
530 else {
531 // write to both output streams. out is the CMSTypedData signer and tee is the file.
532 out.write(b, off, len);
533 tee.write(b, off, len);
534 }
535 }
536
537 @Override
538 public void write(int b) throws IOException {
539 if (closing) {
540 // if the jar is about to close, save the footer that will be written
541 footer.write(b);
542 }
543 else {
544 // write to both output streams. out is the CMSTypedData signer and tee is the file.
545 out.write(b);
546 tee.write(b);
547 }
548 }
549 }
550
551 private static class CMSSigner implements CMSTypedData {
552 private JarFile inputJar;
553 private File publicKeyFile;
554 private X509Certificate publicKey;
555 private PrivateKey privateKey;
556 private String outputFile;
557 private OutputStream outputStream;
558 private final ASN1ObjectIdentifier type;
559 private WholeFileSignerOutputStream signer;
560
561 public CMSSigner(JarFile inputJar, File publicKeyFile,
562 X509Certificate publicKey, PrivateKey privateKey,
563 OutputStream outputStream) {
564 this.inputJar = inputJar;
565 this.publicKeyFile = publicKeyFile;
566 this.publicKey = publicKey;
567 this.privateKey = privateKey;
568 this.outputStream = outputStream;
569 this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
570 }
571
572 public Object getContent() {
573 throw new UnsupportedOperationException();
574 }
575
576 public ASN1ObjectIdentifier getContentType() {
577 return type;
578 }
579
580 public void write(OutputStream out) throws IOException {
581 try {
582 signer = new WholeFileSignerOutputStream(out, outputStream);
583 JarOutputStream outputJar = new JarOutputStream(signer);
584
Kenny Root3d2365c2013-09-19 12:49:36 -0700585 int hash = getDigestAlgorithm(publicKey);
Doug Zongker8562fd42013-04-10 09:19:32 -0700586
587 // Assume the certificate is valid for at least an hour.
588 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
589
590 Manifest manifest = addDigestsToManifest(inputJar, hash);
591 copyFiles(manifest, inputJar, outputJar, timestamp);
592 addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
593
Koushik Dutta29706d12012-12-17 22:25:22 -0800594 signFile(manifest, inputJar,
595 new X509Certificate[]{ publicKey },
596 new PrivateKey[]{ privateKey },
597 outputJar);
Koushik Dutta29706d12012-12-17 22:25:22 -0800598
599 signer.notifyClosing();
600 outputJar.close();
601 signer.finish();
602 }
603 catch (Exception e) {
604 throw new IOException(e);
605 }
606 }
607
608 public void writeSignatureBlock(ByteArrayOutputStream temp)
609 throws IOException,
610 CertificateEncodingException,
611 OperatorCreationException,
612 CMSException {
613 SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
614 }
615
616 public WholeFileSignerOutputStream getSigner() {
617 return signer;
618 }
619 }
620
621 private static void signWholeFile(JarFile inputJar, File publicKeyFile,
622 X509Certificate publicKey, PrivateKey privateKey,
623 OutputStream outputStream) throws Exception {
624 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
625 publicKey, privateKey, outputStream);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700626
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700627 ByteArrayOutputStream temp = new ByteArrayOutputStream();
628
629 // put a readable message and a null char at the start of the
630 // archive comment, so that tools that display the comment
631 // (hopefully) show something sensible.
632 // TODO: anything more useful we can put in this message?
633 byte[] message = "signed by SignApk".getBytes("UTF-8");
634 temp.write(message);
635 temp.write(0);
Doug Zongker147626e2012-09-04 13:32:13 -0700636
Koushik Dutta29706d12012-12-17 22:25:22 -0800637 cmsOut.writeSignatureBlock(temp);
638
639 byte[] zipData = cmsOut.getSigner().getTail();
640
641 // For a zip with no archive comment, the
642 // end-of-central-directory record will be 22 bytes long, so
643 // we expect to find the EOCD marker 22 bytes from the end.
644 if (zipData[zipData.length-22] != 0x50 ||
645 zipData[zipData.length-21] != 0x4b ||
646 zipData[zipData.length-20] != 0x05 ||
647 zipData[zipData.length-19] != 0x06) {
648 throw new IllegalArgumentException("zip data already has an archive comment");
649 }
650
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700651 int total_size = temp.size() + 6;
652 if (total_size > 0xffff) {
653 throw new IllegalArgumentException("signature is too big for ZIP file comment");
654 }
655 // signature starts this many bytes from the end of the file
656 int signature_start = total_size - message.length - 1;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700657 temp.write(signature_start & 0xff);
658 temp.write((signature_start >> 8) & 0xff);
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700659 // Why the 0xff bytes? In a zip file with no archive comment,
660 // bytes [-6:-2] of the file are the little-endian offset from
661 // the start of the file to the central directory. So for the
662 // two high bytes to be 0xff 0xff, the archive would have to
Doug Zongker147626e2012-09-04 13:32:13 -0700663 // be nearly 4GB in size. So it's unlikely that a real
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700664 // commentless archive would have 0xffs here, and lets us tell
665 // an old signed archive from a new one.
666 temp.write(0xff);
667 temp.write(0xff);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700668 temp.write(total_size & 0xff);
669 temp.write((total_size >> 8) & 0xff);
670 temp.flush();
671
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700672 // Signature verification checks that the EOCD header is the
673 // last such sequence in the file (to avoid minzip finding a
674 // fake EOCD appended after the signature in its scan). The
675 // odds of producing this sequence by chance are very low, but
676 // let's catch it here if it does.
677 byte[] b = temp.toByteArray();
678 for (int i = 0; i < b.length-3; ++i) {
679 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
680 throw new IllegalArgumentException("found spurious EOCD header at " + i);
681 }
682 }
683
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700684 outputStream.write(total_size & 0xff);
685 outputStream.write((total_size >> 8) & 0xff);
686 temp.writeTo(outputStream);
687 }
688
Koushik Dutta29706d12012-12-17 22:25:22 -0800689 private static void signFile(Manifest manifest, JarFile inputJar,
690 X509Certificate[] publicKey, PrivateKey[] privateKey,
691 JarOutputStream outputJar)
692 throws Exception {
693 // Assume the certificate is valid for at least an hour.
694 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800695
Koushik Dutta29706d12012-12-17 22:25:22 -0800696 // MANIFEST.MF
Doug Zongker8562fd42013-04-10 09:19:32 -0700697 JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
Koushik Dutta29706d12012-12-17 22:25:22 -0800698 je.setTime(timestamp);
699 outputJar.putNextEntry(je);
700 manifest.write(outputJar);
701
702 int numKeys = publicKey.length;
703 for (int k = 0; k < numKeys; ++k) {
704 // CERT.SF / CERT#.SF
705 je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
706 (String.format(CERT_SF_MULTI_NAME, k)));
707 je.setTime(timestamp);
708 outputJar.putNextEntry(je);
709 ByteArrayOutputStream baos = new ByteArrayOutputStream();
Kenny Root3d2365c2013-09-19 12:49:36 -0700710 writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
Koushik Dutta29706d12012-12-17 22:25:22 -0800711 byte[] signedData = baos.toByteArray();
712 outputJar.write(signedData);
713
Kenny Root3d2365c2013-09-19 12:49:36 -0700714 // CERT.{EC,RSA} / CERT#.{EC,RSA}
Kenny Root62ea4a52013-09-25 09:59:10 -0700715 final String keyType = publicKey[k].getPublicKey().getAlgorithm();
Kenny Root3d2365c2013-09-19 12:49:36 -0700716 je = new JarEntry(numKeys == 1 ?
Kenny Root62ea4a52013-09-25 09:59:10 -0700717 (String.format(CERT_SIG_NAME, keyType)) :
718 (String.format(CERT_SIG_MULTI_NAME, k, keyType)));
Koushik Dutta29706d12012-12-17 22:25:22 -0800719 je.setTime(timestamp);
720 outputJar.putNextEntry(je);
721 writeSignatureBlock(new CMSProcessableByteArray(signedData),
722 publicKey[k], privateKey[k], outputJar);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800723 }
724 }
725
Doug Zongkerb14c9762012-10-15 17:10:13 -0700726 private static void usage() {
727 System.err.println("Usage: signapk [-w] " +
728 "publickey.x509[.pem] privatekey.pk8 " +
729 "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
730 "input.jar output.jar");
731 System.exit(2);
732 }
733
The Android Open Source Project88b60792009-03-03 19:28:42 -0800734 public static void main(String[] args) {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700735 if (args.length < 4) usage();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800736
Doug Zongker147626e2012-09-04 13:32:13 -0700737 sBouncyCastleProvider = new BouncyCastleProvider();
738 Security.addProvider(sBouncyCastleProvider);
739
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700740 boolean signWholeFile = false;
741 int argstart = 0;
742 if (args[0].equals("-w")) {
743 signWholeFile = true;
744 argstart = 1;
745 }
746
Doug Zongkerb14c9762012-10-15 17:10:13 -0700747 if ((args.length - argstart) % 2 == 1) usage();
748 int numKeys = ((args.length - argstart) / 2) - 1;
749 if (signWholeFile && numKeys > 1) {
750 System.err.println("Only one key may be used with -w.");
751 System.exit(2);
752 }
753
754 String inputFilename = args[args.length-2];
755 String outputFilename = args[args.length-1];
756
The Android Open Source Project88b60792009-03-03 19:28:42 -0800757 JarFile inputJar = null;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700758 FileOutputStream outputFile = null;
Doug Zongker8562fd42013-04-10 09:19:32 -0700759 int hashes = 0;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800760
761 try {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700762 File firstPublicKeyFile = new File(args[argstart+0]);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800763
Doug Zongkerb14c9762012-10-15 17:10:13 -0700764 X509Certificate[] publicKey = new X509Certificate[numKeys];
Doug Zongker8562fd42013-04-10 09:19:32 -0700765 try {
766 for (int i = 0; i < numKeys; ++i) {
767 int argNum = argstart + i*2;
768 publicKey[i] = readPublicKey(new File(args[argNum]));
Kenny Root3d2365c2013-09-19 12:49:36 -0700769 hashes |= getDigestAlgorithm(publicKey[i]);
Doug Zongker8562fd42013-04-10 09:19:32 -0700770 }
771 } catch (IllegalArgumentException e) {
772 System.err.println(e);
773 System.exit(1);
Doug Zongkerb14c9762012-10-15 17:10:13 -0700774 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800775
Doug Zongkerb14c9762012-10-15 17:10:13 -0700776 // Set the ZIP file timestamp to the starting valid time
777 // of the 0th certificate plus one hour (to match what
778 // we've historically done).
779 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
780
781 PrivateKey[] privateKey = new PrivateKey[numKeys];
782 for (int i = 0; i < numKeys; ++i) {
783 int argNum = argstart + i*2 + 1;
784 privateKey[i] = readPrivateKey(new File(args[argNum]));
785 }
786 inputJar = new JarFile(new File(inputFilename), false); // Don't verify.
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700787
Koushik Dutta29706d12012-12-17 22:25:22 -0800788 outputFile = new FileOutputStream(outputFilename);
789
790
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700791 if (signWholeFile) {
Kenny Root3d2365c2013-09-19 12:49:36 -0700792 if (!"RSA".equalsIgnoreCase(privateKey[0].getAlgorithm())) {
793 System.err.println("Cannot sign OTA packages with non-RSA keys");
794 System.exit(1);
795 }
Koushik Dutta29706d12012-12-17 22:25:22 -0800796 SignApk.signWholeFile(inputJar, firstPublicKeyFile,
797 publicKey[0], privateKey[0], outputFile);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700798 } else {
Koushik Dutta29706d12012-12-17 22:25:22 -0800799 JarOutputStream outputJar = new JarOutputStream(outputFile);
Doug Zongkere6913732012-07-03 15:03:04 -0700800
Koushik Dutta29706d12012-12-17 22:25:22 -0800801 // For signing .apks, use the maximum compression to make
802 // them as small as possible (since they live forever on
803 // the system partition). For OTA packages, use the
804 // default compression level, which is much much faster
805 // and produces output that is only a tiny bit larger
806 // (~0.1% on full OTA packages I tested).
Doug Zongkere6913732012-07-03 15:03:04 -0700807 outputJar.setLevel(9);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800808
Doug Zongker8562fd42013-04-10 09:19:32 -0700809 Manifest manifest = addDigestsToManifest(inputJar, hashes);
810 copyFiles(manifest, inputJar, outputJar, timestamp);
811 signFile(manifest, inputJar, publicKey, privateKey, outputJar);
Koushik Dutta29706d12012-12-17 22:25:22 -0800812 outputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700813 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800814 } catch (Exception e) {
815 e.printStackTrace();
816 System.exit(1);
817 } finally {
818 try {
819 if (inputJar != null) inputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700820 if (outputFile != null) outputFile.close();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800821 } catch (IOException e) {
822 e.printStackTrace();
823 System.exit(1);
824 }
825 }
826 }
827}