• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.signapk;
18 
19 import org.bouncycastle.asn1.ASN1InputStream;
20 import org.bouncycastle.asn1.ASN1ObjectIdentifier;
21 import org.bouncycastle.asn1.DEROutputStream;
22 import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
23 import org.bouncycastle.cert.jcajce.JcaCertStore;
24 import org.bouncycastle.cms.CMSException;
25 import org.bouncycastle.cms.CMSProcessableByteArray;
26 import org.bouncycastle.cms.CMSSignedData;
27 import org.bouncycastle.cms.CMSSignedDataGenerator;
28 import org.bouncycastle.cms.CMSTypedData;
29 import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
30 import org.bouncycastle.jce.provider.BouncyCastleProvider;
31 import org.bouncycastle.operator.ContentSigner;
32 import org.bouncycastle.operator.OperatorCreationException;
33 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
34 import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
35 import org.bouncycastle.util.encoders.Base64;
36 
37 import java.io.BufferedReader;
38 import java.io.BufferedOutputStream;
39 import java.io.ByteArrayOutputStream;
40 import java.io.DataInputStream;
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.FileOutputStream;
44 import java.io.FilterOutputStream;
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.io.InputStreamReader;
48 import java.io.OutputStream;
49 import java.io.PrintStream;
50 import java.security.DigestOutputStream;
51 import java.security.GeneralSecurityException;
52 import java.security.Key;
53 import java.security.KeyFactory;
54 import java.security.MessageDigest;
55 import java.security.PrivateKey;
56 import java.security.Provider;
57 import java.security.Security;
58 import java.security.cert.CertificateEncodingException;
59 import java.security.cert.CertificateFactory;
60 import java.security.cert.X509Certificate;
61 import java.security.spec.InvalidKeySpecException;
62 import java.security.spec.KeySpec;
63 import java.security.spec.PKCS8EncodedKeySpec;
64 import java.util.ArrayList;
65 import java.util.Collections;
66 import java.util.Enumeration;
67 import java.util.Map;
68 import java.util.TreeMap;
69 import java.util.jar.Attributes;
70 import java.util.jar.JarEntry;
71 import java.util.jar.JarFile;
72 import java.util.jar.JarOutputStream;
73 import java.util.jar.Manifest;
74 import java.util.regex.Pattern;
75 import javax.crypto.Cipher;
76 import javax.crypto.EncryptedPrivateKeyInfo;
77 import javax.crypto.SecretKeyFactory;
78 import javax.crypto.spec.PBEKeySpec;
79 
80 /**
81  * Command line tool to sign JAR files (including APKs and OTA updates) in
82  * a way compatible with the mincrypt verifier, using SHA1 and RSA keys.
83  */
84 class SignApk {
85     private static final String CERT_SF_NAME = "META-INF/CERT.SF";
86     private static final String CERT_RSA_NAME = "META-INF/CERT.RSA";
87     private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
88     private static final String CERT_RSA_MULTI_NAME = "META-INF/CERT%d.RSA";
89 
90     private static final String OTACERT_NAME = "META-INF/com/android/otacert";
91 
92     private static Provider sBouncyCastleProvider;
93 
94     // Files matching this pattern are not copied to the output.
95     private static Pattern stripPattern =
96         Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA)|com/android/otacert))|(" +
97                         Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
98 
readPublicKey(File file)99     private static X509Certificate readPublicKey(File file)
100         throws IOException, GeneralSecurityException {
101         FileInputStream input = new FileInputStream(file);
102         try {
103             CertificateFactory cf = CertificateFactory.getInstance("X.509");
104             return (X509Certificate) cf.generateCertificate(input);
105         } finally {
106             input.close();
107         }
108     }
109 
110     /**
111      * Reads the password from stdin and returns it as a string.
112      *
113      * @param keyFile The file containing the private key.  Used to prompt the user.
114      */
readPassword(File keyFile)115     private static String readPassword(File keyFile) {
116         // TODO: use Console.readPassword() when it's available.
117         System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
118         System.out.flush();
119         BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
120         try {
121             return stdin.readLine();
122         } catch (IOException ex) {
123             return null;
124         }
125     }
126 
127     /**
128      * Decrypt an encrypted PKCS 8 format private key.
129      *
130      * Based on ghstark's post on Aug 6, 2006 at
131      * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
132      *
133      * @param encryptedPrivateKey The raw data of the private key
134      * @param keyFile The file containing the private key
135      */
decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)136     private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
137         throws GeneralSecurityException {
138         EncryptedPrivateKeyInfo epkInfo;
139         try {
140             epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
141         } catch (IOException ex) {
142             // Probably not an encrypted key.
143             return null;
144         }
145 
146         char[] password = readPassword(keyFile).toCharArray();
147 
148         SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
149         Key key = skFactory.generateSecret(new PBEKeySpec(password));
150 
151         Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
152         cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
153 
154         try {
155             return epkInfo.getKeySpec(cipher);
156         } catch (InvalidKeySpecException ex) {
157             System.err.println("signapk: Password for " + keyFile + " may be bad.");
158             throw ex;
159         }
160     }
161 
162     /** Read a PKCS 8 format private key. */
readPrivateKey(File file)163     private static PrivateKey readPrivateKey(File file)
164         throws IOException, GeneralSecurityException {
165         DataInputStream input = new DataInputStream(new FileInputStream(file));
166         try {
167             byte[] bytes = new byte[(int) file.length()];
168             input.read(bytes);
169 
170             KeySpec spec = decryptPrivateKey(bytes, file);
171             if (spec == null) {
172                 spec = new PKCS8EncodedKeySpec(bytes);
173             }
174 
175             try {
176                 return KeyFactory.getInstance("RSA").generatePrivate(spec);
177             } catch (InvalidKeySpecException ex) {
178                 return KeyFactory.getInstance("DSA").generatePrivate(spec);
179             }
180         } finally {
181             input.close();
182         }
183     }
184 
185     /** Add the SHA1 of every file to the manifest, creating it if necessary. */
addDigestsToManifest(JarFile jar)186     private static Manifest addDigestsToManifest(JarFile jar)
187         throws IOException, GeneralSecurityException {
188         Manifest input = jar.getManifest();
189         Manifest output = new Manifest();
190         Attributes main = output.getMainAttributes();
191         if (input != null) {
192             main.putAll(input.getMainAttributes());
193         } else {
194             main.putValue("Manifest-Version", "1.0");
195             main.putValue("Created-By", "1.0 (Android SignApk)");
196         }
197 
198         MessageDigest md = MessageDigest.getInstance("SHA1");
199         byte[] buffer = new byte[4096];
200         int num;
201 
202         // We sort the input entries by name, and add them to the
203         // output manifest in sorted order.  We expect that the output
204         // map will be deterministic.
205 
206         TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
207 
208         for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
209             JarEntry entry = e.nextElement();
210             byName.put(entry.getName(), entry);
211         }
212 
213         for (JarEntry entry: byName.values()) {
214             String name = entry.getName();
215             if (!entry.isDirectory() &&
216                 (stripPattern == null || !stripPattern.matcher(name).matches())) {
217                 InputStream data = jar.getInputStream(entry);
218                 while ((num = data.read(buffer)) > 0) {
219                     md.update(buffer, 0, num);
220                 }
221 
222                 Attributes attr = null;
223                 if (input != null) attr = input.getAttributes(name);
224                 attr = attr != null ? new Attributes(attr) : new Attributes();
225                 attr.putValue("SHA1-Digest",
226                               new String(Base64.encode(md.digest()), "ASCII"));
227                 output.getEntries().put(name, attr);
228             }
229         }
230 
231         return output;
232     }
233 
234     /**
235      * Add a copy of the public key to the archive; this should
236      * exactly match one of the files in
237      * /system/etc/security/otacerts.zip on the device.  (The same
238      * cert can be extracted from the CERT.RSA file but this is much
239      * easier to get at.)
240      */
addOtacert(JarOutputStream outputJar, File publicKeyFile, long timestamp, Manifest manifest)241     private static void addOtacert(JarOutputStream outputJar,
242                                    File publicKeyFile,
243                                    long timestamp,
244                                    Manifest manifest)
245         throws IOException, GeneralSecurityException {
246         MessageDigest md = MessageDigest.getInstance("SHA1");
247 
248         JarEntry je = new JarEntry(OTACERT_NAME);
249         je.setTime(timestamp);
250         outputJar.putNextEntry(je);
251         FileInputStream input = new FileInputStream(publicKeyFile);
252         byte[] b = new byte[4096];
253         int read;
254         while ((read = input.read(b)) != -1) {
255             outputJar.write(b, 0, read);
256             md.update(b, 0, read);
257         }
258         input.close();
259 
260         Attributes attr = new Attributes();
261         attr.putValue("SHA1-Digest",
262                       new String(Base64.encode(md.digest()), "ASCII"));
263         manifest.getEntries().put(OTACERT_NAME, attr);
264     }
265 
266 
267     /** Write to another stream and track how many bytes have been
268      *  written.
269      */
270     private static class CountOutputStream extends FilterOutputStream {
271         private int mCount;
272 
CountOutputStream(OutputStream out)273         public CountOutputStream(OutputStream out) {
274             super(out);
275             mCount = 0;
276         }
277 
278         @Override
write(int b)279         public void write(int b) throws IOException {
280             super.write(b);
281             mCount++;
282         }
283 
284         @Override
write(byte[] b, int off, int len)285         public void write(byte[] b, int off, int len) throws IOException {
286             super.write(b, off, len);
287             mCount += len;
288         }
289 
size()290         public int size() {
291             return mCount;
292         }
293     }
294 
295     /** Write a .SF file with a digest of the specified manifest. */
writeSignatureFile(Manifest manifest, OutputStream out)296     private static void writeSignatureFile(Manifest manifest, OutputStream out)
297         throws IOException, GeneralSecurityException {
298         Manifest sf = new Manifest();
299         Attributes main = sf.getMainAttributes();
300         main.putValue("Signature-Version", "1.0");
301         main.putValue("Created-By", "1.0 (Android SignApk)");
302 
303         MessageDigest md = MessageDigest.getInstance("SHA1");
304         PrintStream print = new PrintStream(
305             new DigestOutputStream(new ByteArrayOutputStream(), md),
306             true, "UTF-8");
307 
308         // Digest of the entire manifest
309         manifest.write(print);
310         print.flush();
311         main.putValue("SHA1-Digest-Manifest",
312                       new String(Base64.encode(md.digest()), "ASCII"));
313 
314         Map<String, Attributes> entries = manifest.getEntries();
315         for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
316             // Digest of the manifest stanza for this entry.
317             print.print("Name: " + entry.getKey() + "\r\n");
318             for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
319                 print.print(att.getKey() + ": " + att.getValue() + "\r\n");
320             }
321             print.print("\r\n");
322             print.flush();
323 
324             Attributes sfAttr = new Attributes();
325             sfAttr.putValue("SHA1-Digest",
326                             new String(Base64.encode(md.digest()), "ASCII"));
327             sf.getEntries().put(entry.getKey(), sfAttr);
328         }
329 
330         CountOutputStream cout = new CountOutputStream(out);
331         sf.write(cout);
332 
333         // A bug in the java.util.jar implementation of Android platforms
334         // up to version 1.6 will cause a spurious IOException to be thrown
335         // if the length of the signature file is a multiple of 1024 bytes.
336         // As a workaround, add an extra CRLF in this case.
337         if ((cout.size() % 1024) == 0) {
338             cout.write('\r');
339             cout.write('\n');
340         }
341     }
342 
343     /** Sign data and write the digital signature to 'out'. */
writeSignatureBlock( CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, OutputStream out)344     private static void writeSignatureBlock(
345         CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
346         OutputStream out)
347         throws IOException,
348                CertificateEncodingException,
349                OperatorCreationException,
350                CMSException {
351         ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
352         certList.add(publicKey);
353         JcaCertStore certs = new JcaCertStore(certList);
354 
355         CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
356         ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA")
357             .setProvider(sBouncyCastleProvider)
358             .build(privateKey);
359         gen.addSignerInfoGenerator(
360             new JcaSignerInfoGeneratorBuilder(
361                 new JcaDigestCalculatorProviderBuilder()
362                 .setProvider(sBouncyCastleProvider)
363                 .build())
364             .setDirectSignature(true)
365             .build(sha1Signer, publicKey));
366         gen.addCertificates(certs);
367         CMSSignedData sigData = gen.generate(data, false);
368 
369         ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
370         DEROutputStream dos = new DEROutputStream(out);
371         dos.writeObject(asn1.readObject());
372     }
373 
374     /**
375      * Copy all the files in a manifest from input to output.  We set
376      * the modification times in the output to a fixed time, so as to
377      * reduce variation in the output file and make incremental OTAs
378      * more efficient.
379      */
copyFiles(Manifest manifest, JarFile in, JarOutputStream out, long timestamp)380     private static void copyFiles(Manifest manifest,
381                                   JarFile in, JarOutputStream out, long timestamp) throws IOException {
382         byte[] buffer = new byte[4096];
383         int num;
384 
385         Map<String, Attributes> entries = manifest.getEntries();
386         ArrayList<String> names = new ArrayList<String>(entries.keySet());
387         Collections.sort(names);
388         for (String name : names) {
389             JarEntry inEntry = in.getJarEntry(name);
390             JarEntry outEntry = null;
391             if (inEntry.getMethod() == JarEntry.STORED) {
392                 // Preserve the STORED method of the input entry.
393                 outEntry = new JarEntry(inEntry);
394             } else {
395                 // Create a new entry so that the compressed len is recomputed.
396                 outEntry = new JarEntry(name);
397             }
398             outEntry.setTime(timestamp);
399             out.putNextEntry(outEntry);
400 
401             InputStream data = in.getInputStream(inEntry);
402             while ((num = data.read(buffer)) > 0) {
403                 out.write(buffer, 0, num);
404             }
405             out.flush();
406         }
407     }
408 
409     private static class WholeFileSignerOutputStream extends FilterOutputStream {
410         private boolean closing = false;
411         private ByteArrayOutputStream footer = new ByteArrayOutputStream();
412         private OutputStream tee;
413 
WholeFileSignerOutputStream(OutputStream out, OutputStream tee)414         public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
415             super(out);
416             this.tee = tee;
417         }
418 
notifyClosing()419         public void notifyClosing() {
420             closing = true;
421         }
422 
finish()423         public void finish() throws IOException {
424             closing = false;
425 
426             byte[] data = footer.toByteArray();
427             if (data.length < 2)
428                 throw new IOException("Less than two bytes written to footer");
429             write(data, 0, data.length - 2);
430         }
431 
getTail()432         public byte[] getTail() {
433             return footer.toByteArray();
434         }
435 
436         @Override
write(byte[] b)437         public void write(byte[] b) throws IOException {
438             write(b, 0, b.length);
439         }
440 
441         @Override
write(byte[] b, int off, int len)442         public void write(byte[] b, int off, int len) throws IOException {
443             if (closing) {
444                 // if the jar is about to close, save the footer that will be written
445                 footer.write(b, off, len);
446             }
447             else {
448                 // write to both output streams. out is the CMSTypedData signer and tee is the file.
449                 out.write(b, off, len);
450                 tee.write(b, off, len);
451             }
452         }
453 
454         @Override
write(int b)455         public void write(int b) throws IOException {
456             if (closing) {
457                 // if the jar is about to close, save the footer that will be written
458                 footer.write(b);
459             }
460             else {
461                 // write to both output streams. out is the CMSTypedData signer and tee is the file.
462                 out.write(b);
463                 tee.write(b);
464             }
465         }
466     }
467 
468     private static class CMSSigner implements CMSTypedData {
469         private JarFile inputJar;
470         private File publicKeyFile;
471         private X509Certificate publicKey;
472         private PrivateKey privateKey;
473         private String outputFile;
474         private OutputStream outputStream;
475         private final ASN1ObjectIdentifier type;
476         private WholeFileSignerOutputStream signer;
477 
CMSSigner(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, OutputStream outputStream)478         public CMSSigner(JarFile inputJar, File publicKeyFile,
479                          X509Certificate publicKey, PrivateKey privateKey,
480                          OutputStream outputStream) {
481             this.inputJar = inputJar;
482             this.publicKeyFile = publicKeyFile;
483             this.publicKey = publicKey;
484             this.privateKey = privateKey;
485             this.outputStream = outputStream;
486             this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
487         }
488 
getContent()489         public Object getContent() {
490             throw new UnsupportedOperationException();
491         }
492 
getContentType()493         public ASN1ObjectIdentifier getContentType() {
494             return type;
495         }
496 
write(OutputStream out)497         public void write(OutputStream out) throws IOException {
498             try {
499                 signer = new WholeFileSignerOutputStream(out, outputStream);
500                 JarOutputStream outputJar = new JarOutputStream(signer);
501 
502                 Manifest manifest = addDigestsToManifest(inputJar);
503                 signFile(manifest, inputJar,
504                          new X509Certificate[]{ publicKey },
505                          new PrivateKey[]{ privateKey },
506                          outputJar);
507                 // Assume the certificate is valid for at least an hour.
508                 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
509                 addOtacert(outputJar, publicKeyFile, timestamp, manifest);
510 
511                 signer.notifyClosing();
512                 outputJar.close();
513                 signer.finish();
514             }
515             catch (Exception e) {
516                 throw new IOException(e);
517             }
518         }
519 
writeSignatureBlock(ByteArrayOutputStream temp)520         public void writeSignatureBlock(ByteArrayOutputStream temp)
521             throws IOException,
522                    CertificateEncodingException,
523                    OperatorCreationException,
524                    CMSException {
525             SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
526         }
527 
getSigner()528         public WholeFileSignerOutputStream getSigner() {
529             return signer;
530         }
531     }
532 
signWholeFile(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, OutputStream outputStream)533     private static void signWholeFile(JarFile inputJar, File publicKeyFile,
534                                       X509Certificate publicKey, PrivateKey privateKey,
535                                       OutputStream outputStream) throws Exception {
536         CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
537                                          publicKey, privateKey, outputStream);
538 
539         ByteArrayOutputStream temp = new ByteArrayOutputStream();
540 
541         // put a readable message and a null char at the start of the
542         // archive comment, so that tools that display the comment
543         // (hopefully) show something sensible.
544         // TODO: anything more useful we can put in this message?
545         byte[] message = "signed by SignApk".getBytes("UTF-8");
546         temp.write(message);
547         temp.write(0);
548 
549         cmsOut.writeSignatureBlock(temp);
550 
551         byte[] zipData = cmsOut.getSigner().getTail();
552 
553         // For a zip with no archive comment, the
554         // end-of-central-directory record will be 22 bytes long, so
555         // we expect to find the EOCD marker 22 bytes from the end.
556         if (zipData[zipData.length-22] != 0x50 ||
557             zipData[zipData.length-21] != 0x4b ||
558             zipData[zipData.length-20] != 0x05 ||
559             zipData[zipData.length-19] != 0x06) {
560             throw new IllegalArgumentException("zip data already has an archive comment");
561         }
562 
563         int total_size = temp.size() + 6;
564         if (total_size > 0xffff) {
565             throw new IllegalArgumentException("signature is too big for ZIP file comment");
566         }
567         // signature starts this many bytes from the end of the file
568         int signature_start = total_size - message.length - 1;
569         temp.write(signature_start & 0xff);
570         temp.write((signature_start >> 8) & 0xff);
571         // Why the 0xff bytes?  In a zip file with no archive comment,
572         // bytes [-6:-2] of the file are the little-endian offset from
573         // the start of the file to the central directory.  So for the
574         // two high bytes to be 0xff 0xff, the archive would have to
575         // be nearly 4GB in size.  So it's unlikely that a real
576         // commentless archive would have 0xffs here, and lets us tell
577         // an old signed archive from a new one.
578         temp.write(0xff);
579         temp.write(0xff);
580         temp.write(total_size & 0xff);
581         temp.write((total_size >> 8) & 0xff);
582         temp.flush();
583 
584         // Signature verification checks that the EOCD header is the
585         // last such sequence in the file (to avoid minzip finding a
586         // fake EOCD appended after the signature in its scan).  The
587         // odds of producing this sequence by chance are very low, but
588         // let's catch it here if it does.
589         byte[] b = temp.toByteArray();
590         for (int i = 0; i < b.length-3; ++i) {
591             if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
592                 throw new IllegalArgumentException("found spurious EOCD header at " + i);
593             }
594         }
595 
596         outputStream.write(total_size & 0xff);
597         outputStream.write((total_size >> 8) & 0xff);
598         temp.writeTo(outputStream);
599     }
600 
signFile(Manifest manifest, JarFile inputJar, X509Certificate[] publicKey, PrivateKey[] privateKey, JarOutputStream outputJar)601     private static void signFile(Manifest manifest, JarFile inputJar,
602                                  X509Certificate[] publicKey, PrivateKey[] privateKey,
603                                  JarOutputStream outputJar)
604         throws Exception {
605         // Assume the certificate is valid for at least an hour.
606         long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
607 
608         JarEntry je;
609 
610         // Everything else
611         copyFiles(manifest, inputJar, outputJar, timestamp);
612 
613         // MANIFEST.MF
614         je = new JarEntry(JarFile.MANIFEST_NAME);
615         je.setTime(timestamp);
616         outputJar.putNextEntry(je);
617         manifest.write(outputJar);
618 
619         int numKeys = publicKey.length;
620         for (int k = 0; k < numKeys; ++k) {
621             // CERT.SF / CERT#.SF
622             je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
623                               (String.format(CERT_SF_MULTI_NAME, k)));
624             je.setTime(timestamp);
625             outputJar.putNextEntry(je);
626             ByteArrayOutputStream baos = new ByteArrayOutputStream();
627             writeSignatureFile(manifest, baos);
628             byte[] signedData = baos.toByteArray();
629             outputJar.write(signedData);
630 
631             // CERT.RSA / CERT#.RSA
632             je = new JarEntry(numKeys == 1 ? CERT_RSA_NAME :
633                               (String.format(CERT_RSA_MULTI_NAME, k)));
634             je.setTime(timestamp);
635             outputJar.putNextEntry(je);
636             writeSignatureBlock(new CMSProcessableByteArray(signedData),
637                                 publicKey[k], privateKey[k], outputJar);
638         }
639     }
640 
usage()641     private static void usage() {
642         System.err.println("Usage: signapk [-w] " +
643                            "publickey.x509[.pem] privatekey.pk8 " +
644                            "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
645                            "input.jar output.jar");
646         System.exit(2);
647     }
648 
main(String[] args)649     public static void main(String[] args) {
650         if (args.length < 4) usage();
651 
652         sBouncyCastleProvider = new BouncyCastleProvider();
653         Security.addProvider(sBouncyCastleProvider);
654 
655         boolean signWholeFile = false;
656         int argstart = 0;
657         if (args[0].equals("-w")) {
658             signWholeFile = true;
659             argstart = 1;
660         }
661 
662         if ((args.length - argstart) % 2 == 1) usage();
663         int numKeys = ((args.length - argstart) / 2) - 1;
664         if (signWholeFile && numKeys > 1) {
665             System.err.println("Only one key may be used with -w.");
666             System.exit(2);
667         }
668 
669         String inputFilename = args[args.length-2];
670         String outputFilename = args[args.length-1];
671 
672         JarFile inputJar = null;
673         FileOutputStream outputFile = null;
674 
675         try {
676             File firstPublicKeyFile = new File(args[argstart+0]);
677 
678             X509Certificate[] publicKey = new X509Certificate[numKeys];
679             for (int i = 0; i < numKeys; ++i) {
680                 int argNum = argstart + i*2;
681                 publicKey[i] = readPublicKey(new File(args[argNum]));
682             }
683 
684             // Set the ZIP file timestamp to the starting valid time
685             // of the 0th certificate plus one hour (to match what
686             // we've historically done).
687             long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
688 
689             PrivateKey[] privateKey = new PrivateKey[numKeys];
690             for (int i = 0; i < numKeys; ++i) {
691                 int argNum = argstart + i*2 + 1;
692                 privateKey[i] = readPrivateKey(new File(args[argNum]));
693             }
694             inputJar = new JarFile(new File(inputFilename), false);  // Don't verify.
695 
696             outputFile = new FileOutputStream(outputFilename);
697 
698 
699             if (signWholeFile) {
700                 SignApk.signWholeFile(inputJar, firstPublicKeyFile,
701                                       publicKey[0], privateKey[0], outputFile);
702             } else {
703                 JarOutputStream outputJar = new JarOutputStream(outputFile);
704 
705                 // For signing .apks, use the maximum compression to make
706                 // them as small as possible (since they live forever on
707                 // the system partition).  For OTA packages, use the
708                 // default compression level, which is much much faster
709                 // and produces output that is only a tiny bit larger
710                 // (~0.1% on full OTA packages I tested).
711                 outputJar.setLevel(9);
712 
713                 signFile(addDigestsToManifest(inputJar), inputJar,
714                          publicKey, privateKey, outputJar);
715                 outputJar.close();
716             }
717         } catch (Exception e) {
718             e.printStackTrace();
719             System.exit(1);
720         } finally {
721             try {
722                 if (inputJar != null) inputJar.close();
723                 if (outputFile != null) outputFile.close();
724             } catch (IOException e) {
725                 e.printStackTrace();
726                 System.exit(1);
727             }
728         }
729     }
730 }
731