blob: e661e506f45b92d671c6156b9bfa4c978d5d6443 [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;
Kenny Root89c961a2013-09-25 11:14:33 -070051import java.lang.reflect.Constructor;
The Android Open Source Project88b60792009-03-03 19:28:42 -080052import java.security.DigestOutputStream;
53import java.security.GeneralSecurityException;
54import java.security.Key;
55import java.security.KeyFactory;
56import java.security.MessageDigest;
57import java.security.PrivateKey;
Doug Zongker147626e2012-09-04 13:32:13 -070058import java.security.Provider;
59import 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;
The Android Open Source Project88b60792009-03-03 19:28:42 -080064import java.security.spec.PKCS8EncodedKeySpec;
65import java.util.ArrayList;
66import java.util.Collections;
The Android Open Source Project88b60792009-03-03 19:28:42 -080067import java.util.Enumeration;
Kenny Root3d2365c2013-09-19 12:49:36 -070068import java.util.Locale;
The Android Open Source Project88b60792009-03-03 19:28:42 -080069import java.util.Map;
70import java.util.TreeMap;
71import java.util.jar.Attributes;
72import java.util.jar.JarEntry;
73import java.util.jar.JarFile;
74import java.util.jar.JarOutputStream;
75import java.util.jar.Manifest;
Doug Zongkeraf482b62009-06-08 10:46:55 -070076import java.util.regex.Pattern;
The Android Open Source Project88b60792009-03-03 19:28:42 -080077import javax.crypto.Cipher;
78import javax.crypto.EncryptedPrivateKeyInfo;
79import javax.crypto.SecretKeyFactory;
80import javax.crypto.spec.PBEKeySpec;
81
82/**
Doug Zongker8562fd42013-04-10 09:19:32 -070083 * HISTORICAL NOTE:
84 *
85 * Prior to the keylimepie release, SignApk ignored the signature
86 * algorithm specified in the certificate and always used SHA1withRSA.
87 *
Kenny Root3d2365c2013-09-19 12:49:36 -070088 * Starting with JB-MR2, the platform supports SHA256withRSA, so we use
89 * the signature algorithm in the certificate to select which to use
90 * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported.
Doug Zongker8562fd42013-04-10 09:19:32 -070091 *
92 * Because there are old keys still in use whose certificate actually
93 * says "MD5withRSA", we treat these as though they say "SHA1withRSA"
94 * for compatibility with older releases. This can be changed by
95 * altering the getAlgorithm() function below.
96 */
97
98
99/**
Kenny Root3d2365c2013-09-19 12:49:36 -0700100 * Command line tool to sign JAR files (including APKs and OTA updates) in a way
101 * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or
102 * SHA-256 (see historical note).
The Android Open Source Project88b60792009-03-03 19:28:42 -0800103 */
104class SignApk {
105 private static final String CERT_SF_NAME = "META-INF/CERT.SF";
Kenny Root3d2365c2013-09-19 12:49:36 -0700106 private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
Doug Zongkerb14c9762012-10-15 17:10:13 -0700107 private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
Kenny Root3d2365c2013-09-19 12:49:36 -0700108 private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s";
The Android Open Source Project88b60792009-03-03 19:28:42 -0800109
Doug Zongker7bb04232012-05-11 09:20:50 -0700110 private static final String OTACERT_NAME = "META-INF/com/android/otacert";
111
Doug Zongker147626e2012-09-04 13:32:13 -0700112 private static Provider sBouncyCastleProvider;
113
Doug Zongker8562fd42013-04-10 09:19:32 -0700114 // bitmasks for which hash algorithms we need the manifest to include.
115 private static final int USE_SHA1 = 1;
116 private static final int USE_SHA256 = 2;
117
118 /**
119 * Return one of USE_SHA1 or USE_SHA256 according to the signature
120 * algorithm specified in the cert.
121 */
Kenny Root3d2365c2013-09-19 12:49:36 -0700122 private static int getDigestAlgorithm(X509Certificate cert) {
123 String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
124 if ("SHA1WITHRSA".equals(sigAlg) ||
125 "MD5WITHRSA".equals(sigAlg)) { // see "HISTORICAL NOTE" above.
Doug Zongker8562fd42013-04-10 09:19:32 -0700126 return USE_SHA1;
Kenny Root3d2365c2013-09-19 12:49:36 -0700127 } else if (sigAlg.startsWith("SHA256WITH")) {
Doug Zongker8562fd42013-04-10 09:19:32 -0700128 return USE_SHA256;
129 } else {
130 throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
131 "\" in cert [" + cert.getSubjectDN());
132 }
133 }
134
Kenny Root3d2365c2013-09-19 12:49:36 -0700135 /** Returns the expected signature algorithm for this key type. */
136 private static String getSignatureAlgorithm(X509Certificate cert) {
137 String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
138 String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
139 if ("RSA".equalsIgnoreCase(keyType)) {
140 if (getDigestAlgorithm(cert) == USE_SHA256) {
141 return "SHA256withRSA";
142 } else {
143 return "SHA1withRSA";
144 }
145 } else if ("EC".equalsIgnoreCase(keyType)) {
146 return "SHA256withECDSA";
147 } else {
148 throw new IllegalArgumentException("unsupported key type: " + keyType);
149 }
150 }
151
Doug Zongkeraf482b62009-06-08 10:46:55 -0700152 // Files matching this pattern are not copied to the output.
153 private static Pattern stripPattern =
Kenny Root3d2365c2013-09-19 12:49:36 -0700154 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
Doug Zongkerb14c9762012-10-15 17:10:13 -0700155 Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
Doug Zongkeraf482b62009-06-08 10:46:55 -0700156
The Android Open Source Project88b60792009-03-03 19:28:42 -0800157 private static X509Certificate readPublicKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800158 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800159 FileInputStream input = new FileInputStream(file);
160 try {
161 CertificateFactory cf = CertificateFactory.getInstance("X.509");
162 return (X509Certificate) cf.generateCertificate(input);
163 } finally {
164 input.close();
165 }
166 }
167
168 /**
169 * Reads the password from stdin and returns it as a string.
170 *
171 * @param keyFile The file containing the private key. Used to prompt the user.
172 */
173 private static String readPassword(File keyFile) {
174 // TODO: use Console.readPassword() when it's available.
175 System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
176 System.out.flush();
177 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
178 try {
179 return stdin.readLine();
180 } catch (IOException ex) {
181 return null;
182 }
183 }
184
185 /**
Kenny Root62ea4a52013-09-25 09:59:10 -0700186 * Decrypt an encrypted PKCS#8 format private key.
The Android Open Source Project88b60792009-03-03 19:28:42 -0800187 *
188 * Based on ghstark's post on Aug 6, 2006 at
189 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
190 *
191 * @param encryptedPrivateKey The raw data of the private key
192 * @param keyFile The file containing the private key
193 */
Kenny Root62ea4a52013-09-25 09:59:10 -0700194 private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
Koushik Dutta29706d12012-12-17 22:25:22 -0800195 throws GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800196 EncryptedPrivateKeyInfo epkInfo;
197 try {
198 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
199 } catch (IOException ex) {
200 // Probably not an encrypted key.
201 return null;
202 }
203
204 char[] password = readPassword(keyFile).toCharArray();
205
206 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
207 Key key = skFactory.generateSecret(new PBEKeySpec(password));
208
209 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
210 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
211
212 try {
213 return epkInfo.getKeySpec(cipher);
214 } catch (InvalidKeySpecException ex) {
215 System.err.println("signapk: Password for " + keyFile + " may be bad.");
216 throw ex;
217 }
218 }
219
Kenny Root62ea4a52013-09-25 09:59:10 -0700220 /** Read a PKCS#8 format private key. */
The Android Open Source Project88b60792009-03-03 19:28:42 -0800221 private static PrivateKey readPrivateKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800222 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800223 DataInputStream input = new DataInputStream(new FileInputStream(file));
224 try {
225 byte[] bytes = new byte[(int) file.length()];
226 input.read(bytes);
227
Kenny Root62ea4a52013-09-25 09:59:10 -0700228 /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
229 PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800230 if (spec == null) {
231 spec = new PKCS8EncodedKeySpec(bytes);
232 }
233
Kenny Root62ea4a52013-09-25 09:59:10 -0700234 /*
235 * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
236 * OID and use that to construct a KeyFactory.
237 */
238 ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()));
239 PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject());
240 String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
Kenny Root3d2365c2013-09-19 12:49:36 -0700241
Kenny Root62ea4a52013-09-25 09:59:10 -0700242 return KeyFactory.getInstance(algOid).generatePrivate(spec);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800243 } finally {
244 input.close();
245 }
246 }
247
Doug Zongker8562fd42013-04-10 09:19:32 -0700248 /**
249 * Add the hash(es) of every file to the manifest, creating it if
250 * necessary.
251 */
252 private static Manifest addDigestsToManifest(JarFile jar, int hashes)
Koushik Dutta29706d12012-12-17 22:25:22 -0800253 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800254 Manifest input = jar.getManifest();
255 Manifest output = new Manifest();
256 Attributes main = output.getMainAttributes();
257 if (input != null) {
258 main.putAll(input.getMainAttributes());
259 } else {
260 main.putValue("Manifest-Version", "1.0");
261 main.putValue("Created-By", "1.0 (Android SignApk)");
262 }
263
Doug Zongker8562fd42013-04-10 09:19:32 -0700264 MessageDigest md_sha1 = null;
265 MessageDigest md_sha256 = null;
266 if ((hashes & USE_SHA1) != 0) {
267 md_sha1 = MessageDigest.getInstance("SHA1");
268 }
269 if ((hashes & USE_SHA256) != 0) {
270 md_sha256 = MessageDigest.getInstance("SHA256");
271 }
272
The Android Open Source Project88b60792009-03-03 19:28:42 -0800273 byte[] buffer = new byte[4096];
274 int num;
275
276 // We sort the input entries by name, and add them to the
277 // output manifest in sorted order. We expect that the output
278 // map will be deterministic.
279
280 TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
281
282 for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
283 JarEntry entry = e.nextElement();
284 byName.put(entry.getName(), entry);
285 }
286
287 for (JarEntry entry: byName.values()) {
288 String name = entry.getName();
Doug Zongkerb14c9762012-10-15 17:10:13 -0700289 if (!entry.isDirectory() &&
290 (stripPattern == null || !stripPattern.matcher(name).matches())) {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800291 InputStream data = jar.getInputStream(entry);
292 while ((num = data.read(buffer)) > 0) {
Doug Zongker8562fd42013-04-10 09:19:32 -0700293 if (md_sha1 != null) md_sha1.update(buffer, 0, num);
294 if (md_sha256 != null) md_sha256.update(buffer, 0, num);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800295 }
296
297 Attributes attr = null;
298 if (input != null) attr = input.getAttributes(name);
299 attr = attr != null ? new Attributes(attr) : new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700300 if (md_sha1 != null) {
301 attr.putValue("SHA1-Digest",
302 new String(Base64.encode(md_sha1.digest()), "ASCII"));
303 }
304 if (md_sha256 != null) {
305 attr.putValue("SHA-256-Digest",
306 new String(Base64.encode(md_sha256.digest()), "ASCII"));
307 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800308 output.getEntries().put(name, attr);
309 }
310 }
311
312 return output;
313 }
314
Doug Zongker7bb04232012-05-11 09:20:50 -0700315 /**
316 * Add a copy of the public key to the archive; this should
317 * exactly match one of the files in
318 * /system/etc/security/otacerts.zip on the device. (The same
319 * cert can be extracted from the CERT.RSA file but this is much
320 * easier to get at.)
321 */
322 private static void addOtacert(JarOutputStream outputJar,
323 File publicKeyFile,
324 long timestamp,
Doug Zongker8562fd42013-04-10 09:19:32 -0700325 Manifest manifest,
326 int hash)
Doug Zongker7bb04232012-05-11 09:20:50 -0700327 throws IOException, GeneralSecurityException {
Doug Zongker8562fd42013-04-10 09:19:32 -0700328 MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256");
Doug Zongker7bb04232012-05-11 09:20:50 -0700329
330 JarEntry je = new JarEntry(OTACERT_NAME);
331 je.setTime(timestamp);
332 outputJar.putNextEntry(je);
333 FileInputStream input = new FileInputStream(publicKeyFile);
334 byte[] b = new byte[4096];
335 int read;
336 while ((read = input.read(b)) != -1) {
337 outputJar.write(b, 0, read);
338 md.update(b, 0, read);
339 }
340 input.close();
341
342 Attributes attr = new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700343 attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest",
Doug Zongker147626e2012-09-04 13:32:13 -0700344 new String(Base64.encode(md.digest()), "ASCII"));
Doug Zongker7bb04232012-05-11 09:20:50 -0700345 manifest.getEntries().put(OTACERT_NAME, attr);
346 }
347
348
Doug Zongker147626e2012-09-04 13:32:13 -0700349 /** Write to another stream and track how many bytes have been
350 * written.
351 */
352 private static class CountOutputStream extends FilterOutputStream {
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700353 private int mCount;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800354
Doug Zongker147626e2012-09-04 13:32:13 -0700355 public CountOutputStream(OutputStream out) {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800356 super(out);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700357 mCount = 0;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800358 }
359
360 @Override
361 public void write(int b) throws IOException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800362 super.write(b);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700363 mCount++;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800364 }
365
366 @Override
367 public void write(byte[] b, int off, int len) throws IOException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800368 super.write(b, off, len);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700369 mCount += len;
370 }
371
372 public int size() {
373 return mCount;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800374 }
375 }
376
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700377 /** Write a .SF file with a digest of the specified manifest. */
Doug Zongker8562fd42013-04-10 09:19:32 -0700378 private static void writeSignatureFile(Manifest manifest, OutputStream out,
379 int hash)
Doug Zongker147626e2012-09-04 13:32:13 -0700380 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800381 Manifest sf = new Manifest();
382 Attributes main = sf.getMainAttributes();
383 main.putValue("Signature-Version", "1.0");
384 main.putValue("Created-By", "1.0 (Android SignApk)");
385
Doug Zongker8562fd42013-04-10 09:19:32 -0700386 MessageDigest md = MessageDigest.getInstance(
387 hash == USE_SHA256 ? "SHA256" : "SHA1");
The Android Open Source Project88b60792009-03-03 19:28:42 -0800388 PrintStream print = new PrintStream(
Koushik Dutta29706d12012-12-17 22:25:22 -0800389 new DigestOutputStream(new ByteArrayOutputStream(), md),
390 true, "UTF-8");
The Android Open Source Project88b60792009-03-03 19:28:42 -0800391
392 // Digest of the entire manifest
393 manifest.write(print);
394 print.flush();
Doug Zongker8562fd42013-04-10 09:19:32 -0700395 main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
Doug Zongker147626e2012-09-04 13:32:13 -0700396 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800397
398 Map<String, Attributes> entries = manifest.getEntries();
399 for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
400 // Digest of the manifest stanza for this entry.
401 print.print("Name: " + entry.getKey() + "\r\n");
402 for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
403 print.print(att.getKey() + ": " + att.getValue() + "\r\n");
404 }
405 print.print("\r\n");
406 print.flush();
407
408 Attributes sfAttr = new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700409 sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest",
Doug Zongker147626e2012-09-04 13:32:13 -0700410 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800411 sf.getEntries().put(entry.getKey(), sfAttr);
412 }
413
Doug Zongker147626e2012-09-04 13:32:13 -0700414 CountOutputStream cout = new CountOutputStream(out);
415 sf.write(cout);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700416
417 // A bug in the java.util.jar implementation of Android platforms
418 // up to version 1.6 will cause a spurious IOException to be thrown
419 // if the length of the signature file is a multiple of 1024 bytes.
420 // As a workaround, add an extra CRLF in this case.
Doug Zongker147626e2012-09-04 13:32:13 -0700421 if ((cout.size() % 1024) == 0) {
422 cout.write('\r');
423 cout.write('\n');
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700424 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800425 }
426
Doug Zongker147626e2012-09-04 13:32:13 -0700427 /** Sign data and write the digital signature to 'out'. */
The Android Open Source Project88b60792009-03-03 19:28:42 -0800428 private static void writeSignatureBlock(
Doug Zongker147626e2012-09-04 13:32:13 -0700429 CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
430 OutputStream out)
431 throws IOException,
432 CertificateEncodingException,
433 OperatorCreationException,
434 CMSException {
435 ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
436 certList.add(publicKey);
437 JcaCertStore certs = new JcaCertStore(certList);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800438
Doug Zongker147626e2012-09-04 13:32:13 -0700439 CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
Kenny Root3d2365c2013-09-19 12:49:36 -0700440 ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
Doug Zongker147626e2012-09-04 13:32:13 -0700441 .setProvider(sBouncyCastleProvider)
442 .build(privateKey);
443 gen.addSignerInfoGenerator(
444 new JcaSignerInfoGeneratorBuilder(
445 new JcaDigestCalculatorProviderBuilder()
446 .setProvider(sBouncyCastleProvider)
447 .build())
448 .setDirectSignature(true)
Doug Zongker8562fd42013-04-10 09:19:32 -0700449 .build(signer, publicKey));
Doug Zongker147626e2012-09-04 13:32:13 -0700450 gen.addCertificates(certs);
451 CMSSignedData sigData = gen.generate(data, false);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800452
Doug Zongker147626e2012-09-04 13:32:13 -0700453 ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
454 DEROutputStream dos = new DEROutputStream(out);
455 dos.writeObject(asn1.readObject());
The Android Open Source Project88b60792009-03-03 19:28:42 -0800456 }
457
Koushik Dutta29706d12012-12-17 22:25:22 -0800458 /**
459 * Copy all the files in a manifest from input to output. We set
460 * the modification times in the output to a fixed time, so as to
461 * reduce variation in the output file and make incremental OTAs
462 * more efficient.
463 */
Doug Zongker1d67eec2014-05-15 09:54:26 -0700464 private static void copyFiles(Manifest manifest, JarFile in, JarOutputStream out,
465 long timestamp, int alignment) throws IOException {
Koushik Dutta29706d12012-12-17 22:25:22 -0800466 byte[] buffer = new byte[4096];
467 int num;
468
469 Map<String, Attributes> entries = manifest.getEntries();
470 ArrayList<String> names = new ArrayList<String>(entries.keySet());
471 Collections.sort(names);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700472
473 boolean firstEntry = true;
474 long offset = 0L;
475
476 // We do the copy in two passes -- first copying all the
477 // entries that are STORED, then copying all the entries that
478 // have any other compression flag (which in practice means
479 // DEFLATED). This groups all the stored entries together at
480 // the start of the file and makes it easier to do alignment
481 // on them (since only stored entries are aligned).
482
Koushik Dutta29706d12012-12-17 22:25:22 -0800483 for (String name : names) {
484 JarEntry inEntry = in.getJarEntry(name);
485 JarEntry outEntry = null;
Doug Zongker1d67eec2014-05-15 09:54:26 -0700486 if (inEntry.getMethod() != JarEntry.STORED) continue;
487 // Preserve the STORED method of the input entry.
488 outEntry = new JarEntry(inEntry);
489 outEntry.setTime(timestamp);
490
491 // 'offset' is the offset into the file at which we expect
492 // the file data to begin. This is the value we need to
493 // make a multiple of 'alignement'.
494 offset += JarFile.LOCHDR + outEntry.getName().length();
495 if (firstEntry) {
496 // The first entry in a jar file has an extra field of
497 // four bytes that you can't get rid of; any extra
498 // data you specify in the JarEntry is appended to
499 // these forced four bytes. This is JAR_MAGIC in
500 // JarOutputStream; the bytes are 0xfeca0000.
501 offset += 4;
502 firstEntry = false;
Koushik Dutta29706d12012-12-17 22:25:22 -0800503 }
Doug Zongker1d67eec2014-05-15 09:54:26 -0700504 if (alignment > 0 && (offset % alignment != 0)) {
505 // Set the "extra data" of the entry to between 1 and
506 // alignment-1 bytes, to make the file data begin at
507 // an aligned offset.
508 int needed = alignment - (int)(offset % alignment);
509 outEntry.setExtra(new byte[needed]);
510 offset += needed;
511 }
512
513 out.putNextEntry(outEntry);
514
515 InputStream data = in.getInputStream(inEntry);
516 while ((num = data.read(buffer)) > 0) {
517 out.write(buffer, 0, num);
518 offset += num;
519 }
520 out.flush();
521 }
522
523 // Copy all the non-STORED entries. We don't attempt to
524 // maintain the 'offset' variable past this point; we don't do
525 // alignment on these entries.
526
527 for (String name : names) {
528 JarEntry inEntry = in.getJarEntry(name);
529 JarEntry outEntry = null;
530 if (inEntry.getMethod() == JarEntry.STORED) continue;
531 // Create a new entry so that the compressed len is recomputed.
532 outEntry = new JarEntry(name);
Koushik Dutta29706d12012-12-17 22:25:22 -0800533 outEntry.setTime(timestamp);
534 out.putNextEntry(outEntry);
535
536 InputStream data = in.getInputStream(inEntry);
537 while ((num = data.read(buffer)) > 0) {
538 out.write(buffer, 0, num);
539 }
540 out.flush();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700541 }
Koushik Dutta29706d12012-12-17 22:25:22 -0800542 }
543
544 private static class WholeFileSignerOutputStream extends FilterOutputStream {
545 private boolean closing = false;
546 private ByteArrayOutputStream footer = new ByteArrayOutputStream();
547 private OutputStream tee;
548
549 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
550 super(out);
551 this.tee = tee;
552 }
553
554 public void notifyClosing() {
555 closing = true;
556 }
557
558 public void finish() throws IOException {
559 closing = false;
560
561 byte[] data = footer.toByteArray();
562 if (data.length < 2)
563 throw new IOException("Less than two bytes written to footer");
564 write(data, 0, data.length - 2);
565 }
566
567 public byte[] getTail() {
568 return footer.toByteArray();
569 }
570
571 @Override
572 public void write(byte[] b) throws IOException {
573 write(b, 0, b.length);
574 }
575
576 @Override
577 public void write(byte[] b, int off, int len) throws IOException {
578 if (closing) {
579 // if the jar is about to close, save the footer that will be written
580 footer.write(b, off, len);
581 }
582 else {
583 // write to both output streams. out is the CMSTypedData signer and tee is the file.
584 out.write(b, off, len);
585 tee.write(b, off, len);
586 }
587 }
588
589 @Override
590 public void write(int b) throws IOException {
591 if (closing) {
592 // if the jar is about to close, save the footer that will be written
593 footer.write(b);
594 }
595 else {
596 // write to both output streams. out is the CMSTypedData signer and tee is the file.
597 out.write(b);
598 tee.write(b);
599 }
600 }
601 }
602
603 private static class CMSSigner implements CMSTypedData {
604 private JarFile inputJar;
605 private File publicKeyFile;
606 private X509Certificate publicKey;
607 private PrivateKey privateKey;
608 private String outputFile;
609 private OutputStream outputStream;
610 private final ASN1ObjectIdentifier type;
611 private WholeFileSignerOutputStream signer;
612
613 public CMSSigner(JarFile inputJar, File publicKeyFile,
614 X509Certificate publicKey, PrivateKey privateKey,
615 OutputStream outputStream) {
616 this.inputJar = inputJar;
617 this.publicKeyFile = publicKeyFile;
618 this.publicKey = publicKey;
619 this.privateKey = privateKey;
620 this.outputStream = outputStream;
621 this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
622 }
623
624 public Object getContent() {
625 throw new UnsupportedOperationException();
626 }
627
628 public ASN1ObjectIdentifier getContentType() {
629 return type;
630 }
631
632 public void write(OutputStream out) throws IOException {
633 try {
634 signer = new WholeFileSignerOutputStream(out, outputStream);
635 JarOutputStream outputJar = new JarOutputStream(signer);
636
Kenny Root3d2365c2013-09-19 12:49:36 -0700637 int hash = getDigestAlgorithm(publicKey);
Doug Zongker8562fd42013-04-10 09:19:32 -0700638
639 // Assume the certificate is valid for at least an hour.
640 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
641
642 Manifest manifest = addDigestsToManifest(inputJar, hash);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700643 copyFiles(manifest, inputJar, outputJar, timestamp, 0);
Doug Zongker8562fd42013-04-10 09:19:32 -0700644 addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
645
Koushik Dutta29706d12012-12-17 22:25:22 -0800646 signFile(manifest, inputJar,
647 new X509Certificate[]{ publicKey },
648 new PrivateKey[]{ privateKey },
649 outputJar);
Koushik Dutta29706d12012-12-17 22:25:22 -0800650
651 signer.notifyClosing();
652 outputJar.close();
653 signer.finish();
654 }
655 catch (Exception e) {
656 throw new IOException(e);
657 }
658 }
659
660 public void writeSignatureBlock(ByteArrayOutputStream temp)
661 throws IOException,
662 CertificateEncodingException,
663 OperatorCreationException,
664 CMSException {
665 SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
666 }
667
668 public WholeFileSignerOutputStream getSigner() {
669 return signer;
670 }
671 }
672
673 private static void signWholeFile(JarFile inputJar, File publicKeyFile,
674 X509Certificate publicKey, PrivateKey privateKey,
675 OutputStream outputStream) throws Exception {
676 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
677 publicKey, privateKey, outputStream);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700678
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700679 ByteArrayOutputStream temp = new ByteArrayOutputStream();
680
681 // put a readable message and a null char at the start of the
682 // archive comment, so that tools that display the comment
683 // (hopefully) show something sensible.
684 // TODO: anything more useful we can put in this message?
685 byte[] message = "signed by SignApk".getBytes("UTF-8");
686 temp.write(message);
687 temp.write(0);
Doug Zongker147626e2012-09-04 13:32:13 -0700688
Koushik Dutta29706d12012-12-17 22:25:22 -0800689 cmsOut.writeSignatureBlock(temp);
690
691 byte[] zipData = cmsOut.getSigner().getTail();
692
693 // For a zip with no archive comment, the
694 // end-of-central-directory record will be 22 bytes long, so
695 // we expect to find the EOCD marker 22 bytes from the end.
696 if (zipData[zipData.length-22] != 0x50 ||
697 zipData[zipData.length-21] != 0x4b ||
698 zipData[zipData.length-20] != 0x05 ||
699 zipData[zipData.length-19] != 0x06) {
700 throw new IllegalArgumentException("zip data already has an archive comment");
701 }
702
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700703 int total_size = temp.size() + 6;
704 if (total_size > 0xffff) {
705 throw new IllegalArgumentException("signature is too big for ZIP file comment");
706 }
707 // signature starts this many bytes from the end of the file
708 int signature_start = total_size - message.length - 1;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700709 temp.write(signature_start & 0xff);
710 temp.write((signature_start >> 8) & 0xff);
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700711 // Why the 0xff bytes? In a zip file with no archive comment,
712 // bytes [-6:-2] of the file are the little-endian offset from
713 // the start of the file to the central directory. So for the
714 // two high bytes to be 0xff 0xff, the archive would have to
Doug Zongker147626e2012-09-04 13:32:13 -0700715 // be nearly 4GB in size. So it's unlikely that a real
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700716 // commentless archive would have 0xffs here, and lets us tell
717 // an old signed archive from a new one.
718 temp.write(0xff);
719 temp.write(0xff);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700720 temp.write(total_size & 0xff);
721 temp.write((total_size >> 8) & 0xff);
722 temp.flush();
723
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700724 // Signature verification checks that the EOCD header is the
725 // last such sequence in the file (to avoid minzip finding a
726 // fake EOCD appended after the signature in its scan). The
727 // odds of producing this sequence by chance are very low, but
728 // let's catch it here if it does.
729 byte[] b = temp.toByteArray();
730 for (int i = 0; i < b.length-3; ++i) {
731 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
732 throw new IllegalArgumentException("found spurious EOCD header at " + i);
733 }
734 }
735
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700736 outputStream.write(total_size & 0xff);
737 outputStream.write((total_size >> 8) & 0xff);
738 temp.writeTo(outputStream);
739 }
740
Koushik Dutta29706d12012-12-17 22:25:22 -0800741 private static void signFile(Manifest manifest, JarFile inputJar,
742 X509Certificate[] publicKey, PrivateKey[] privateKey,
743 JarOutputStream outputJar)
744 throws Exception {
745 // Assume the certificate is valid for at least an hour.
746 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800747
Koushik Dutta29706d12012-12-17 22:25:22 -0800748 // MANIFEST.MF
Doug Zongker8562fd42013-04-10 09:19:32 -0700749 JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
Koushik Dutta29706d12012-12-17 22:25:22 -0800750 je.setTime(timestamp);
751 outputJar.putNextEntry(je);
752 manifest.write(outputJar);
753
754 int numKeys = publicKey.length;
755 for (int k = 0; k < numKeys; ++k) {
756 // CERT.SF / CERT#.SF
757 je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
758 (String.format(CERT_SF_MULTI_NAME, k)));
759 je.setTime(timestamp);
760 outputJar.putNextEntry(je);
761 ByteArrayOutputStream baos = new ByteArrayOutputStream();
Kenny Root3d2365c2013-09-19 12:49:36 -0700762 writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
Koushik Dutta29706d12012-12-17 22:25:22 -0800763 byte[] signedData = baos.toByteArray();
764 outputJar.write(signedData);
765
Kenny Root3d2365c2013-09-19 12:49:36 -0700766 // CERT.{EC,RSA} / CERT#.{EC,RSA}
Kenny Root62ea4a52013-09-25 09:59:10 -0700767 final String keyType = publicKey[k].getPublicKey().getAlgorithm();
Kenny Root3d2365c2013-09-19 12:49:36 -0700768 je = new JarEntry(numKeys == 1 ?
Kenny Root62ea4a52013-09-25 09:59:10 -0700769 (String.format(CERT_SIG_NAME, keyType)) :
770 (String.format(CERT_SIG_MULTI_NAME, k, keyType)));
Koushik Dutta29706d12012-12-17 22:25:22 -0800771 je.setTime(timestamp);
772 outputJar.putNextEntry(je);
773 writeSignatureBlock(new CMSProcessableByteArray(signedData),
774 publicKey[k], privateKey[k], outputJar);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800775 }
776 }
777
Kenny Root89c961a2013-09-25 11:14:33 -0700778 /**
779 * Tries to load a JSE Provider by class name. This is for custom PrivateKey
780 * types that might be stored in PKCS#11-like storage.
781 */
782 private static void loadProviderIfNecessary(String providerClassName) {
783 if (providerClassName == null) {
784 return;
785 }
786
787 final Class<?> klass;
788 try {
789 final ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
790 if (sysLoader != null) {
791 klass = sysLoader.loadClass(providerClassName);
792 } else {
793 klass = Class.forName(providerClassName);
794 }
795 } catch (ClassNotFoundException e) {
796 e.printStackTrace();
797 System.exit(1);
798 return;
799 }
800
801 Constructor<?> constructor = null;
802 for (Constructor<?> c : klass.getConstructors()) {
803 if (c.getParameterTypes().length == 0) {
804 constructor = c;
805 break;
806 }
807 }
808 if (constructor == null) {
809 System.err.println("No zero-arg constructor found for " + providerClassName);
810 System.exit(1);
811 return;
812 }
813
814 final Object o;
815 try {
816 o = constructor.newInstance();
817 } catch (Exception e) {
818 e.printStackTrace();
819 System.exit(1);
820 return;
821 }
822 if (!(o instanceof Provider)) {
823 System.err.println("Not a Provider class: " + providerClassName);
824 System.exit(1);
825 }
826
827 Security.insertProviderAt((Provider) o, 1);
828 }
829
Doug Zongkerb14c9762012-10-15 17:10:13 -0700830 private static void usage() {
831 System.err.println("Usage: signapk [-w] " +
Doug Zongker1d67eec2014-05-15 09:54:26 -0700832 "[-a <alignment>] " +
Kenny Root89c961a2013-09-25 11:14:33 -0700833 "[-providerClass <className>] " +
Doug Zongkerb14c9762012-10-15 17:10:13 -0700834 "publickey.x509[.pem] privatekey.pk8 " +
835 "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
836 "input.jar output.jar");
837 System.exit(2);
838 }
839
The Android Open Source Project88b60792009-03-03 19:28:42 -0800840 public static void main(String[] args) {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700841 if (args.length < 4) usage();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800842
Doug Zongker147626e2012-09-04 13:32:13 -0700843 sBouncyCastleProvider = new BouncyCastleProvider();
844 Security.addProvider(sBouncyCastleProvider);
845
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700846 boolean signWholeFile = false;
Kenny Root89c961a2013-09-25 11:14:33 -0700847 String providerClass = null;
848 String providerArg = null;
Doug Zongker1d67eec2014-05-15 09:54:26 -0700849 int alignment = 4;
Kenny Root89c961a2013-09-25 11:14:33 -0700850
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700851 int argstart = 0;
Kenny Root89c961a2013-09-25 11:14:33 -0700852 while (argstart < args.length && args[argstart].startsWith("-")) {
853 if ("-w".equals(args[argstart])) {
854 signWholeFile = true;
855 ++argstart;
856 } else if ("-providerClass".equals(args[argstart])) {
857 if (argstart + 1 >= args.length) {
858 usage();
859 }
860 providerClass = args[++argstart];
861 ++argstart;
Doug Zongker1d67eec2014-05-15 09:54:26 -0700862 } else if ("-a".equals(args[argstart])) {
863 alignment = Integer.parseInt(args[++argstart]);
864 ++argstart;
Kenny Root89c961a2013-09-25 11:14:33 -0700865 } else {
866 usage();
867 }
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700868 }
869
Doug Zongkerb14c9762012-10-15 17:10:13 -0700870 if ((args.length - argstart) % 2 == 1) usage();
871 int numKeys = ((args.length - argstart) / 2) - 1;
872 if (signWholeFile && numKeys > 1) {
873 System.err.println("Only one key may be used with -w.");
874 System.exit(2);
875 }
876
Kenny Root89c961a2013-09-25 11:14:33 -0700877 loadProviderIfNecessary(providerClass);
878
Doug Zongkerb14c9762012-10-15 17:10:13 -0700879 String inputFilename = args[args.length-2];
880 String outputFilename = args[args.length-1];
881
The Android Open Source Project88b60792009-03-03 19:28:42 -0800882 JarFile inputJar = null;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700883 FileOutputStream outputFile = null;
Doug Zongker8562fd42013-04-10 09:19:32 -0700884 int hashes = 0;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800885
886 try {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700887 File firstPublicKeyFile = new File(args[argstart+0]);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800888
Doug Zongkerb14c9762012-10-15 17:10:13 -0700889 X509Certificate[] publicKey = new X509Certificate[numKeys];
Doug Zongker8562fd42013-04-10 09:19:32 -0700890 try {
891 for (int i = 0; i < numKeys; ++i) {
892 int argNum = argstart + i*2;
893 publicKey[i] = readPublicKey(new File(args[argNum]));
Kenny Root3d2365c2013-09-19 12:49:36 -0700894 hashes |= getDigestAlgorithm(publicKey[i]);
Doug Zongker8562fd42013-04-10 09:19:32 -0700895 }
896 } catch (IllegalArgumentException e) {
897 System.err.println(e);
898 System.exit(1);
Doug Zongkerb14c9762012-10-15 17:10:13 -0700899 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800900
Doug Zongkerb14c9762012-10-15 17:10:13 -0700901 // Set the ZIP file timestamp to the starting valid time
902 // of the 0th certificate plus one hour (to match what
903 // we've historically done).
904 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
905
906 PrivateKey[] privateKey = new PrivateKey[numKeys];
907 for (int i = 0; i < numKeys; ++i) {
908 int argNum = argstart + i*2 + 1;
909 privateKey[i] = readPrivateKey(new File(args[argNum]));
910 }
911 inputJar = new JarFile(new File(inputFilename), false); // Don't verify.
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700912
Koushik Dutta29706d12012-12-17 22:25:22 -0800913 outputFile = new FileOutputStream(outputFilename);
914
915
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700916 if (signWholeFile) {
Koushik Dutta29706d12012-12-17 22:25:22 -0800917 SignApk.signWholeFile(inputJar, firstPublicKeyFile,
918 publicKey[0], privateKey[0], outputFile);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700919 } else {
Koushik Dutta29706d12012-12-17 22:25:22 -0800920 JarOutputStream outputJar = new JarOutputStream(outputFile);
Doug Zongkere6913732012-07-03 15:03:04 -0700921
Koushik Dutta29706d12012-12-17 22:25:22 -0800922 // For signing .apks, use the maximum compression to make
923 // them as small as possible (since they live forever on
924 // the system partition). For OTA packages, use the
925 // default compression level, which is much much faster
926 // and produces output that is only a tiny bit larger
927 // (~0.1% on full OTA packages I tested).
Doug Zongkere6913732012-07-03 15:03:04 -0700928 outputJar.setLevel(9);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800929
Doug Zongker8562fd42013-04-10 09:19:32 -0700930 Manifest manifest = addDigestsToManifest(inputJar, hashes);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700931 copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
Doug Zongker8562fd42013-04-10 09:19:32 -0700932 signFile(manifest, inputJar, publicKey, privateKey, outputJar);
Koushik Dutta29706d12012-12-17 22:25:22 -0800933 outputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700934 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800935 } catch (Exception e) {
936 e.printStackTrace();
937 System.exit(1);
938 } finally {
939 try {
940 if (inputJar != null) inputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700941 if (outputFile != null) outputFile.close();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800942 } catch (IOException e) {
943 e.printStackTrace();
944 System.exit(1);
945 }
946 }
947 }
948}