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.asn1.pkcs.PrivateKeyInfo;
24 import org.bouncycastle.cert.jcajce.JcaCertStore;
25 import org.bouncycastle.cms.CMSException;
26 import org.bouncycastle.cms.CMSProcessableByteArray;
27 import org.bouncycastle.cms.CMSSignedData;
28 import org.bouncycastle.cms.CMSSignedDataGenerator;
29 import org.bouncycastle.cms.CMSTypedData;
30 import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
31 import org.bouncycastle.jce.provider.BouncyCastleProvider;
32 import org.bouncycastle.operator.ContentSigner;
33 import org.bouncycastle.operator.OperatorCreationException;
34 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
35 import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
36 import org.bouncycastle.util.encoders.Base64;
37 
38 import java.io.Console;
39 import java.io.BufferedReader;
40 import java.io.ByteArrayInputStream;
41 import java.io.ByteArrayOutputStream;
42 import java.io.DataInputStream;
43 import java.io.File;
44 import java.io.FileInputStream;
45 import java.io.FileOutputStream;
46 import java.io.FilterOutputStream;
47 import java.io.IOException;
48 import java.io.InputStream;
49 import java.io.InputStreamReader;
50 import java.io.OutputStream;
51 import java.io.PrintStream;
52 import java.lang.reflect.Constructor;
53 import java.security.DigestOutputStream;
54 import java.security.GeneralSecurityException;
55 import java.security.Key;
56 import java.security.KeyFactory;
57 import java.security.MessageDigest;
58 import java.security.PrivateKey;
59 import java.security.Provider;
60 import java.security.Security;
61 import java.security.cert.CertificateEncodingException;
62 import java.security.cert.CertificateFactory;
63 import java.security.cert.X509Certificate;
64 import java.security.spec.InvalidKeySpecException;
65 import java.security.spec.PKCS8EncodedKeySpec;
66 import java.util.ArrayList;
67 import java.util.Collections;
68 import java.util.Enumeration;
69 import java.util.Locale;
70 import java.util.Map;
71 import java.util.TreeMap;
72 import java.util.jar.Attributes;
73 import java.util.jar.JarEntry;
74 import java.util.jar.JarFile;
75 import java.util.jar.JarOutputStream;
76 import java.util.jar.Manifest;
77 import java.util.regex.Pattern;
78 import javax.crypto.Cipher;
79 import javax.crypto.EncryptedPrivateKeyInfo;
80 import javax.crypto.SecretKeyFactory;
81 import javax.crypto.spec.PBEKeySpec;
82 
83 /**
84  * HISTORICAL NOTE:
85  *
86  * Prior to the keylimepie release, SignApk ignored the signature
87  * algorithm specified in the certificate and always used SHA1withRSA.
88  *
89  * Starting with JB-MR2, the platform supports SHA256withRSA, so we use
90  * the signature algorithm in the certificate to select which to use
91  * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported.
92  *
93  * Because there are old keys still in use whose certificate actually
94  * says "MD5withRSA", we treat these as though they say "SHA1withRSA"
95  * for compatibility with older releases.  This can be changed by
96  * altering the getAlgorithm() function below.
97  */
98 
99 
100 /**
101  * Command line tool to sign JAR files (including APKs and OTA updates) in a way
102  * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or
103  * SHA-256 (see historical note).
104  */
105 class SignApk {
106     private static final String CERT_SF_NAME = "META-INF/CERT.SF";
107     private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
108     private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
109     private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s";
110 
111     private static final String OTACERT_NAME = "META-INF/com/android/otacert";
112 
113     private static Provider sBouncyCastleProvider;
114 
115     // bitmasks for which hash algorithms we need the manifest to include.
116     private static final int USE_SHA1 = 1;
117     private static final int USE_SHA256 = 2;
118 
119     /**
120      * Return one of USE_SHA1 or USE_SHA256 according to the signature
121      * algorithm specified in the cert.
122      */
getDigestAlgorithm(X509Certificate cert)123     private static int getDigestAlgorithm(X509Certificate cert) {
124         String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
125         if ("SHA1WITHRSA".equals(sigAlg) ||
126             "MD5WITHRSA".equals(sigAlg)) {     // see "HISTORICAL NOTE" above.
127             return USE_SHA1;
128         } else if (sigAlg.startsWith("SHA256WITH")) {
129             return USE_SHA256;
130         } else {
131             throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
132                                                "\" in cert [" + cert.getSubjectDN());
133         }
134     }
135 
136     /** Returns the expected signature algorithm for this key type. */
getSignatureAlgorithm(X509Certificate cert)137     private static String getSignatureAlgorithm(X509Certificate cert) {
138         String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
139         String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
140         if ("RSA".equalsIgnoreCase(keyType)) {
141             if (getDigestAlgorithm(cert) == USE_SHA256) {
142                 return "SHA256withRSA";
143             } else {
144                 return "SHA1withRSA";
145             }
146         } else if ("EC".equalsIgnoreCase(keyType)) {
147             return "SHA256withECDSA";
148         } else {
149             throw new IllegalArgumentException("unsupported key type: " + keyType);
150         }
151     }
152 
153     // Files matching this pattern are not copied to the output.
154     private static Pattern stripPattern =
155         Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
156                         Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
157 
readPublicKey(File file)158     private static X509Certificate readPublicKey(File file)
159         throws IOException, GeneralSecurityException {
160         FileInputStream input = new FileInputStream(file);
161         try {
162             CertificateFactory cf = CertificateFactory.getInstance("X.509");
163             return (X509Certificate) cf.generateCertificate(input);
164         } finally {
165             input.close();
166         }
167     }
168 
169     /**
170      * Reads the password from console and returns it as a string.
171      *
172      * @param keyFile The file containing the private key.  Used to prompt the user.
173      */
readPassword(File keyFile)174     private static String readPassword(File keyFile) {
175         Console console;
176         char[] pwd;
177         if((console = System.console()) != null &&
178            (pwd = console.readPassword("[%s]", "Enter password for " + keyFile)) != null){
179             return String.valueOf(pwd);
180         } else {
181             return null;
182         }
183     }
184 
185     /**
186      * Decrypt an encrypted PKCS#8 format private key.
187      *
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      */
decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)194     private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
195         throws GeneralSecurityException {
196         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 
220     /** Read a PKCS#8 format private key. */
readPrivateKey(File file)221     private static PrivateKey readPrivateKey(File file)
222         throws IOException, GeneralSecurityException {
223         DataInputStream input = new DataInputStream(new FileInputStream(file));
224         try {
225             byte[] bytes = new byte[(int) file.length()];
226             input.read(bytes);
227 
228             /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
229             PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file);
230             if (spec == null) {
231                 spec = new PKCS8EncodedKeySpec(bytes);
232             }
233 
234             /*
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();
241 
242             return KeyFactory.getInstance(algOid).generatePrivate(spec);
243         } finally {
244             input.close();
245         }
246     }
247 
248     /**
249      * Add the hash(es) of every file to the manifest, creating it if
250      * necessary.
251      */
addDigestsToManifest(JarFile jar, int hashes)252     private static Manifest addDigestsToManifest(JarFile jar, int hashes)
253         throws IOException, GeneralSecurityException {
254         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 
264         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 
273         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();
289             if (!entry.isDirectory() &&
290                 (stripPattern == null || !stripPattern.matcher(name).matches())) {
291                 InputStream data = jar.getInputStream(entry);
292                 while ((num = data.read(buffer)) > 0) {
293                     if (md_sha1 != null) md_sha1.update(buffer, 0, num);
294                     if (md_sha256 != null) md_sha256.update(buffer, 0, num);
295                 }
296 
297                 Attributes attr = null;
298                 if (input != null) attr = input.getAttributes(name);
299                 attr = attr != null ? new Attributes(attr) : new Attributes();
300                 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                 }
308                 output.getEntries().put(name, attr);
309             }
310         }
311 
312         return output;
313     }
314 
315     /**
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      */
addOtacert(JarOutputStream outputJar, File publicKeyFile, long timestamp, Manifest manifest, int hash)322     private static void addOtacert(JarOutputStream outputJar,
323                                    File publicKeyFile,
324                                    long timestamp,
325                                    Manifest manifest,
326                                    int hash)
327         throws IOException, GeneralSecurityException {
328         MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256");
329 
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();
343         attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest",
344                       new String(Base64.encode(md.digest()), "ASCII"));
345         manifest.getEntries().put(OTACERT_NAME, attr);
346     }
347 
348 
349     /** Write to another stream and track how many bytes have been
350      *  written.
351      */
352     private static class CountOutputStream extends FilterOutputStream {
353         private int mCount;
354 
CountOutputStream(OutputStream out)355         public CountOutputStream(OutputStream out) {
356             super(out);
357             mCount = 0;
358         }
359 
360         @Override
write(int b)361         public void write(int b) throws IOException {
362             super.write(b);
363             mCount++;
364         }
365 
366         @Override
write(byte[] b, int off, int len)367         public void write(byte[] b, int off, int len) throws IOException {
368             super.write(b, off, len);
369             mCount += len;
370         }
371 
size()372         public int size() {
373             return mCount;
374         }
375     }
376 
377     /** Write a .SF file with a digest of the specified manifest. */
writeSignatureFile(Manifest manifest, OutputStream out, int hash)378     private static void writeSignatureFile(Manifest manifest, OutputStream out,
379                                            int hash)
380         throws IOException, GeneralSecurityException {
381         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 
386         MessageDigest md = MessageDigest.getInstance(
387             hash == USE_SHA256 ? "SHA256" : "SHA1");
388         PrintStream print = new PrintStream(
389             new DigestOutputStream(new ByteArrayOutputStream(), md),
390             true, "UTF-8");
391 
392         // Digest of the entire manifest
393         manifest.write(print);
394         print.flush();
395         main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
396                       new String(Base64.encode(md.digest()), "ASCII"));
397 
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();
409             sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest",
410                             new String(Base64.encode(md.digest()), "ASCII"));
411             sf.getEntries().put(entry.getKey(), sfAttr);
412         }
413 
414         CountOutputStream cout = new CountOutputStream(out);
415         sf.write(cout);
416 
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.
421         if ((cout.size() % 1024) == 0) {
422             cout.write('\r');
423             cout.write('\n');
424         }
425     }
426 
427     /** Sign data and write the digital signature to 'out'. */
writeSignatureBlock( CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, OutputStream out)428     private static void writeSignatureBlock(
429         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);
438 
439         CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
440         ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
441             .setProvider(sBouncyCastleProvider)
442             .build(privateKey);
443         gen.addSignerInfoGenerator(
444             new JcaSignerInfoGeneratorBuilder(
445                 new JcaDigestCalculatorProviderBuilder()
446                 .setProvider(sBouncyCastleProvider)
447                 .build())
448             .setDirectSignature(true)
449             .build(signer, publicKey));
450         gen.addCertificates(certs);
451         CMSSignedData sigData = gen.generate(data, false);
452 
453         ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
454         DEROutputStream dos = new DEROutputStream(out);
455         dos.writeObject(asn1.readObject());
456     }
457 
458     /**
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      */
copyFiles(Manifest manifest, JarFile in, JarOutputStream out, long timestamp, int alignment)464     private static void copyFiles(Manifest manifest, JarFile in, JarOutputStream out,
465                                   long timestamp, int alignment) throws IOException {
466         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);
472 
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 
483         for (String name : names) {
484             JarEntry inEntry = in.getJarEntry(name);
485             JarEntry outEntry = null;
486             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;
503             }
504             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);
533             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();
541         }
542     }
543 
544     private static class WholeFileSignerOutputStream extends FilterOutputStream {
545         private boolean closing = false;
546         private ByteArrayOutputStream footer = new ByteArrayOutputStream();
547         private OutputStream tee;
548 
WholeFileSignerOutputStream(OutputStream out, OutputStream tee)549         public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
550             super(out);
551             this.tee = tee;
552         }
553 
notifyClosing()554         public void notifyClosing() {
555             closing = true;
556         }
557 
finish()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 
getTail()567         public byte[] getTail() {
568             return footer.toByteArray();
569         }
570 
571         @Override
write(byte[] b)572         public void write(byte[] b) throws IOException {
573             write(b, 0, b.length);
574         }
575 
576         @Override
write(byte[] b, int off, int len)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
write(int b)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 
CMSSigner(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, OutputStream outputStream)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         /**
625          * This should actually return byte[] or something similar, but nothing
626          * actually checks it currently.
627          */
getContent()628         public Object getContent() {
629             return this;
630         }
631 
getContentType()632         public ASN1ObjectIdentifier getContentType() {
633             return type;
634         }
635 
write(OutputStream out)636         public void write(OutputStream out) throws IOException {
637             try {
638                 signer = new WholeFileSignerOutputStream(out, outputStream);
639                 JarOutputStream outputJar = new JarOutputStream(signer);
640 
641                 int hash = getDigestAlgorithm(publicKey);
642 
643                 // Assume the certificate is valid for at least an hour.
644                 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
645 
646                 Manifest manifest = addDigestsToManifest(inputJar, hash);
647                 copyFiles(manifest, inputJar, outputJar, timestamp, 0);
648                 addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
649 
650                 signFile(manifest, inputJar,
651                          new X509Certificate[]{ publicKey },
652                          new PrivateKey[]{ privateKey },
653                          outputJar);
654 
655                 signer.notifyClosing();
656                 outputJar.close();
657                 signer.finish();
658             }
659             catch (Exception e) {
660                 throw new IOException(e);
661             }
662         }
663 
writeSignatureBlock(ByteArrayOutputStream temp)664         public void writeSignatureBlock(ByteArrayOutputStream temp)
665             throws IOException,
666                    CertificateEncodingException,
667                    OperatorCreationException,
668                    CMSException {
669             SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
670         }
671 
getSigner()672         public WholeFileSignerOutputStream getSigner() {
673             return signer;
674         }
675     }
676 
signWholeFile(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, OutputStream outputStream)677     private static void signWholeFile(JarFile inputJar, File publicKeyFile,
678                                       X509Certificate publicKey, PrivateKey privateKey,
679                                       OutputStream outputStream) throws Exception {
680         CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
681                                          publicKey, privateKey, outputStream);
682 
683         ByteArrayOutputStream temp = new ByteArrayOutputStream();
684 
685         // put a readable message and a null char at the start of the
686         // archive comment, so that tools that display the comment
687         // (hopefully) show something sensible.
688         // TODO: anything more useful we can put in this message?
689         byte[] message = "signed by SignApk".getBytes("UTF-8");
690         temp.write(message);
691         temp.write(0);
692 
693         cmsOut.writeSignatureBlock(temp);
694 
695         byte[] zipData = cmsOut.getSigner().getTail();
696 
697         // For a zip with no archive comment, the
698         // end-of-central-directory record will be 22 bytes long, so
699         // we expect to find the EOCD marker 22 bytes from the end.
700         if (zipData[zipData.length-22] != 0x50 ||
701             zipData[zipData.length-21] != 0x4b ||
702             zipData[zipData.length-20] != 0x05 ||
703             zipData[zipData.length-19] != 0x06) {
704             throw new IllegalArgumentException("zip data already has an archive comment");
705         }
706 
707         int total_size = temp.size() + 6;
708         if (total_size > 0xffff) {
709             throw new IllegalArgumentException("signature is too big for ZIP file comment");
710         }
711         // signature starts this many bytes from the end of the file
712         int signature_start = total_size - message.length - 1;
713         temp.write(signature_start & 0xff);
714         temp.write((signature_start >> 8) & 0xff);
715         // Why the 0xff bytes?  In a zip file with no archive comment,
716         // bytes [-6:-2] of the file are the little-endian offset from
717         // the start of the file to the central directory.  So for the
718         // two high bytes to be 0xff 0xff, the archive would have to
719         // be nearly 4GB in size.  So it's unlikely that a real
720         // commentless archive would have 0xffs here, and lets us tell
721         // an old signed archive from a new one.
722         temp.write(0xff);
723         temp.write(0xff);
724         temp.write(total_size & 0xff);
725         temp.write((total_size >> 8) & 0xff);
726         temp.flush();
727 
728         // Signature verification checks that the EOCD header is the
729         // last such sequence in the file (to avoid minzip finding a
730         // fake EOCD appended after the signature in its scan).  The
731         // odds of producing this sequence by chance are very low, but
732         // let's catch it here if it does.
733         byte[] b = temp.toByteArray();
734         for (int i = 0; i < b.length-3; ++i) {
735             if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
736                 throw new IllegalArgumentException("found spurious EOCD header at " + i);
737             }
738         }
739 
740         outputStream.write(total_size & 0xff);
741         outputStream.write((total_size >> 8) & 0xff);
742         temp.writeTo(outputStream);
743     }
744 
signFile(Manifest manifest, JarFile inputJar, X509Certificate[] publicKey, PrivateKey[] privateKey, JarOutputStream outputJar)745     private static void signFile(Manifest manifest, JarFile inputJar,
746                                  X509Certificate[] publicKey, PrivateKey[] privateKey,
747                                  JarOutputStream outputJar)
748         throws Exception {
749         // Assume the certificate is valid for at least an hour.
750         long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
751 
752         // MANIFEST.MF
753         JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
754         je.setTime(timestamp);
755         outputJar.putNextEntry(je);
756         manifest.write(outputJar);
757 
758         int numKeys = publicKey.length;
759         for (int k = 0; k < numKeys; ++k) {
760             // CERT.SF / CERT#.SF
761             je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
762                               (String.format(CERT_SF_MULTI_NAME, k)));
763             je.setTime(timestamp);
764             outputJar.putNextEntry(je);
765             ByteArrayOutputStream baos = new ByteArrayOutputStream();
766             writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
767             byte[] signedData = baos.toByteArray();
768             outputJar.write(signedData);
769 
770             // CERT.{EC,RSA} / CERT#.{EC,RSA}
771             final String keyType = publicKey[k].getPublicKey().getAlgorithm();
772             je = new JarEntry(numKeys == 1 ?
773                               (String.format(CERT_SIG_NAME, keyType)) :
774                               (String.format(CERT_SIG_MULTI_NAME, k, keyType)));
775             je.setTime(timestamp);
776             outputJar.putNextEntry(je);
777             writeSignatureBlock(new CMSProcessableByteArray(signedData),
778                                 publicKey[k], privateKey[k], outputJar);
779         }
780     }
781 
782     /**
783      * Tries to load a JSE Provider by class name. This is for custom PrivateKey
784      * types that might be stored in PKCS#11-like storage.
785      */
loadProviderIfNecessary(String providerClassName)786     private static void loadProviderIfNecessary(String providerClassName) {
787         if (providerClassName == null) {
788             return;
789         }
790 
791         final Class<?> klass;
792         try {
793             final ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
794             if (sysLoader != null) {
795                 klass = sysLoader.loadClass(providerClassName);
796             } else {
797                 klass = Class.forName(providerClassName);
798             }
799         } catch (ClassNotFoundException e) {
800             e.printStackTrace();
801             System.exit(1);
802             return;
803         }
804 
805         Constructor<?> constructor = null;
806         for (Constructor<?> c : klass.getConstructors()) {
807             if (c.getParameterTypes().length == 0) {
808                 constructor = c;
809                 break;
810             }
811         }
812         if (constructor == null) {
813             System.err.println("No zero-arg constructor found for " + providerClassName);
814             System.exit(1);
815             return;
816         }
817 
818         final Object o;
819         try {
820             o = constructor.newInstance();
821         } catch (Exception e) {
822             e.printStackTrace();
823             System.exit(1);
824             return;
825         }
826         if (!(o instanceof Provider)) {
827             System.err.println("Not a Provider class: " + providerClassName);
828             System.exit(1);
829         }
830 
831         Security.insertProviderAt((Provider) o, 1);
832     }
833 
usage()834     private static void usage() {
835         System.err.println("Usage: signapk [-w] " +
836                            "[-a <alignment>] " +
837                            "[-providerClass <className>] " +
838                            "publickey.x509[.pem] privatekey.pk8 " +
839                            "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
840                            "input.jar output.jar");
841         System.exit(2);
842     }
843 
main(String[] args)844     public static void main(String[] args) {
845         if (args.length < 4) usage();
846 
847         sBouncyCastleProvider = new BouncyCastleProvider();
848         Security.addProvider(sBouncyCastleProvider);
849 
850         boolean signWholeFile = false;
851         String providerClass = null;
852         String providerArg = null;
853         int alignment = 4;
854 
855         int argstart = 0;
856         while (argstart < args.length && args[argstart].startsWith("-")) {
857             if ("-w".equals(args[argstart])) {
858                 signWholeFile = true;
859                 ++argstart;
860             } else if ("-providerClass".equals(args[argstart])) {
861                 if (argstart + 1 >= args.length) {
862                     usage();
863                 }
864                 providerClass = args[++argstart];
865                 ++argstart;
866             } else if ("-a".equals(args[argstart])) {
867                 alignment = Integer.parseInt(args[++argstart]);
868                 ++argstart;
869             } else {
870                 usage();
871             }
872         }
873 
874         if ((args.length - argstart) % 2 == 1) usage();
875         int numKeys = ((args.length - argstart) / 2) - 1;
876         if (signWholeFile && numKeys > 1) {
877             System.err.println("Only one key may be used with -w.");
878             System.exit(2);
879         }
880 
881         loadProviderIfNecessary(providerClass);
882 
883         String inputFilename = args[args.length-2];
884         String outputFilename = args[args.length-1];
885 
886         JarFile inputJar = null;
887         FileOutputStream outputFile = null;
888         int hashes = 0;
889 
890         try {
891             File firstPublicKeyFile = new File(args[argstart+0]);
892 
893             X509Certificate[] publicKey = new X509Certificate[numKeys];
894             try {
895                 for (int i = 0; i < numKeys; ++i) {
896                     int argNum = argstart + i*2;
897                     publicKey[i] = readPublicKey(new File(args[argNum]));
898                     hashes |= getDigestAlgorithm(publicKey[i]);
899                 }
900             } catch (IllegalArgumentException e) {
901                 System.err.println(e);
902                 System.exit(1);
903             }
904 
905             // Set the ZIP file timestamp to the starting valid time
906             // of the 0th certificate plus one hour (to match what
907             // we've historically done).
908             long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
909 
910             PrivateKey[] privateKey = new PrivateKey[numKeys];
911             for (int i = 0; i < numKeys; ++i) {
912                 int argNum = argstart + i*2 + 1;
913                 privateKey[i] = readPrivateKey(new File(args[argNum]));
914             }
915             inputJar = new JarFile(new File(inputFilename), false);  // Don't verify.
916 
917             outputFile = new FileOutputStream(outputFilename);
918 
919 
920             if (signWholeFile) {
921                 SignApk.signWholeFile(inputJar, firstPublicKeyFile,
922                                       publicKey[0], privateKey[0], outputFile);
923             } else {
924                 JarOutputStream outputJar = new JarOutputStream(outputFile);
925 
926                 // For signing .apks, use the maximum compression to make
927                 // them as small as possible (since they live forever on
928                 // the system partition).  For OTA packages, use the
929                 // default compression level, which is much much faster
930                 // and produces output that is only a tiny bit larger
931                 // (~0.1% on full OTA packages I tested).
932                 outputJar.setLevel(9);
933 
934                 Manifest manifest = addDigestsToManifest(inputJar, hashes);
935                 copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
936                 signFile(manifest, inputJar, publicKey, privateKey, outputJar);
937                 outputJar.close();
938             }
939         } catch (Exception e) {
940             e.printStackTrace();
941             System.exit(1);
942         } finally {
943             try {
944                 if (inputJar != null) inputJar.close();
945                 if (outputFile != null) outputFile.close();
946             } catch (IOException e) {
947                 e.printStackTrace();
948                 System.exit(1);
949             }
950         }
951     }
952 }
953