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