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.BufferedReader;
39 import java.io.ByteArrayInputStream;
40 import java.io.ByteArrayOutputStream;
41 import java.io.DataInputStream;
42 import java.io.File;
43 import java.io.FileInputStream;
44 import java.io.FileOutputStream;
45 import java.io.FilterOutputStream;
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.io.InputStreamReader;
49 import java.io.OutputStream;
50 import java.io.PrintStream;
51 import java.lang.reflect.Constructor;
52 import java.security.DigestOutputStream;
53 import java.security.GeneralSecurityException;
54 import java.security.Key;
55 import java.security.KeyFactory;
56 import java.security.MessageDigest;
57 import java.security.PrivateKey;
58 import java.security.Provider;
59 import java.security.Security;
60 import java.security.cert.CertificateEncodingException;
61 import java.security.cert.CertificateFactory;
62 import java.security.cert.X509Certificate;
63 import java.security.spec.InvalidKeySpecException;
64 import java.security.spec.PKCS8EncodedKeySpec;
65 import java.util.ArrayList;
66 import java.util.Collections;
67 import java.util.Enumeration;
68 import java.util.Locale;
69 import java.util.Map;
70 import java.util.TreeMap;
71 import java.util.jar.Attributes;
72 import java.util.jar.JarEntry;
73 import java.util.jar.JarFile;
74 import java.util.jar.JarOutputStream;
75 import java.util.jar.Manifest;
76 import java.util.regex.Pattern;
77 import javax.crypto.Cipher;
78 import javax.crypto.EncryptedPrivateKeyInfo;
79 import javax.crypto.SecretKeyFactory;
80 import javax.crypto.spec.PBEKeySpec;
81 
82 /**
83  * HISTORICAL NOTE:
84  *
85  * Prior to the keylimepie release, SignApk ignored the signature
86  * algorithm specified in the certificate and always used SHA1withRSA.
87  *
88  * Starting with JB-MR2, the platform supports SHA256withRSA, so we use
89  * the signature algorithm in the certificate to select which to use
90  * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported.
91  *
92  * Because there are old keys still in use whose certificate actually
93  * says "MD5withRSA", we treat these as though they say "SHA1withRSA"
94  * for compatibility with older releases.  This can be changed by
95  * altering the getAlgorithm() function below.
96  */
97 
98 
99 /**
100  * Command line tool to sign JAR files (including APKs and OTA updates) in a way
101  * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or
102  * SHA-256 (see historical note).
103  */
104 class SignApk {
105     private static final String CERT_SF_NAME = "META-INF/CERT.SF";
106     private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
107     private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
108     private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s";
109 
110     private static final String OTACERT_NAME = "META-INF/com/android/otacert";
111 
112     private static Provider sBouncyCastleProvider;
113 
114     // bitmasks for which hash algorithms we need the manifest to include.
115     private static final int USE_SHA1 = 1;
116     private static final int USE_SHA256 = 2;
117 
118     /**
119      * Return one of USE_SHA1 or USE_SHA256 according to the signature
120      * algorithm specified in the cert.
121      */
getDigestAlgorithm(X509Certificate cert)122     private static int getDigestAlgorithm(X509Certificate cert) {
123         String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
124         if ("SHA1WITHRSA".equals(sigAlg) ||
125             "MD5WITHRSA".equals(sigAlg)) {     // see "HISTORICAL NOTE" above.
126             return USE_SHA1;
127         } else if (sigAlg.startsWith("SHA256WITH")) {
128             return USE_SHA256;
129         } else {
130             throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
131                                                "\" in cert [" + cert.getSubjectDN());
132         }
133     }
134 
135     /** Returns the expected signature algorithm for this key type. */
getSignatureAlgorithm(X509Certificate cert)136     private static String getSignatureAlgorithm(X509Certificate cert) {
137         String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
138         String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
139         if ("RSA".equalsIgnoreCase(keyType)) {
140             if (getDigestAlgorithm(cert) == USE_SHA256) {
141                 return "SHA256withRSA";
142             } else {
143                 return "SHA1withRSA";
144             }
145         } else if ("EC".equalsIgnoreCase(keyType)) {
146             return "SHA256withECDSA";
147         } else {
148             throw new IllegalArgumentException("unsupported key type: " + keyType);
149         }
150     }
151 
152     // Files matching this pattern are not copied to the output.
153     private static Pattern stripPattern =
154         Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
155                         Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
156 
readPublicKey(File file)157     private static X509Certificate readPublicKey(File file)
158         throws IOException, GeneralSecurityException {
159         FileInputStream input = new FileInputStream(file);
160         try {
161             CertificateFactory cf = CertificateFactory.getInstance("X.509");
162             return (X509Certificate) cf.generateCertificate(input);
163         } finally {
164             input.close();
165         }
166     }
167 
168     /**
169      * Reads the password from stdin and returns it as a string.
170      *
171      * @param keyFile The file containing the private key.  Used to prompt the user.
172      */
readPassword(File keyFile)173     private static String readPassword(File keyFile) {
174         // TODO: use Console.readPassword() when it's available.
175         System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
176         System.out.flush();
177         BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
178         try {
179             return stdin.readLine();
180         } catch (IOException ex) {
181             return null;
182         }
183     }
184 
185     /**
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 
getContent()624         public Object getContent() {
625             throw new UnsupportedOperationException();
626         }
627 
getContentType()628         public ASN1ObjectIdentifier getContentType() {
629             return type;
630         }
631 
write(OutputStream out)632         public void write(OutputStream out) throws IOException {
633             try {
634                 signer = new WholeFileSignerOutputStream(out, outputStream);
635                 JarOutputStream outputJar = new JarOutputStream(signer);
636 
637                 int hash = getDigestAlgorithm(publicKey);
638 
639                 // Assume the certificate is valid for at least an hour.
640                 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
641 
642                 Manifest manifest = addDigestsToManifest(inputJar, hash);
643                 copyFiles(manifest, inputJar, outputJar, timestamp, 0);
644                 addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
645 
646                 signFile(manifest, inputJar,
647                          new X509Certificate[]{ publicKey },
648                          new PrivateKey[]{ privateKey },
649                          outputJar);
650 
651                 signer.notifyClosing();
652                 outputJar.close();
653                 signer.finish();
654             }
655             catch (Exception e) {
656                 throw new IOException(e);
657             }
658         }
659 
writeSignatureBlock(ByteArrayOutputStream temp)660         public void writeSignatureBlock(ByteArrayOutputStream temp)
661             throws IOException,
662                    CertificateEncodingException,
663                    OperatorCreationException,
664                    CMSException {
665             SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
666         }
667 
getSigner()668         public WholeFileSignerOutputStream getSigner() {
669             return signer;
670         }
671     }
672 
signWholeFile(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, OutputStream outputStream)673     private static void signWholeFile(JarFile inputJar, File publicKeyFile,
674                                       X509Certificate publicKey, PrivateKey privateKey,
675                                       OutputStream outputStream) throws Exception {
676         CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
677                                          publicKey, privateKey, outputStream);
678 
679         ByteArrayOutputStream temp = new ByteArrayOutputStream();
680 
681         // put a readable message and a null char at the start of the
682         // archive comment, so that tools that display the comment
683         // (hopefully) show something sensible.
684         // TODO: anything more useful we can put in this message?
685         byte[] message = "signed by SignApk".getBytes("UTF-8");
686         temp.write(message);
687         temp.write(0);
688 
689         cmsOut.writeSignatureBlock(temp);
690 
691         byte[] zipData = cmsOut.getSigner().getTail();
692 
693         // For a zip with no archive comment, the
694         // end-of-central-directory record will be 22 bytes long, so
695         // we expect to find the EOCD marker 22 bytes from the end.
696         if (zipData[zipData.length-22] != 0x50 ||
697             zipData[zipData.length-21] != 0x4b ||
698             zipData[zipData.length-20] != 0x05 ||
699             zipData[zipData.length-19] != 0x06) {
700             throw new IllegalArgumentException("zip data already has an archive comment");
701         }
702 
703         int total_size = temp.size() + 6;
704         if (total_size > 0xffff) {
705             throw new IllegalArgumentException("signature is too big for ZIP file comment");
706         }
707         // signature starts this many bytes from the end of the file
708         int signature_start = total_size - message.length - 1;
709         temp.write(signature_start & 0xff);
710         temp.write((signature_start >> 8) & 0xff);
711         // Why the 0xff bytes?  In a zip file with no archive comment,
712         // bytes [-6:-2] of the file are the little-endian offset from
713         // the start of the file to the central directory.  So for the
714         // two high bytes to be 0xff 0xff, the archive would have to
715         // be nearly 4GB in size.  So it's unlikely that a real
716         // commentless archive would have 0xffs here, and lets us tell
717         // an old signed archive from a new one.
718         temp.write(0xff);
719         temp.write(0xff);
720         temp.write(total_size & 0xff);
721         temp.write((total_size >> 8) & 0xff);
722         temp.flush();
723 
724         // Signature verification checks that the EOCD header is the
725         // last such sequence in the file (to avoid minzip finding a
726         // fake EOCD appended after the signature in its scan).  The
727         // odds of producing this sequence by chance are very low, but
728         // let's catch it here if it does.
729         byte[] b = temp.toByteArray();
730         for (int i = 0; i < b.length-3; ++i) {
731             if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
732                 throw new IllegalArgumentException("found spurious EOCD header at " + i);
733             }
734         }
735 
736         outputStream.write(total_size & 0xff);
737         outputStream.write((total_size >> 8) & 0xff);
738         temp.writeTo(outputStream);
739     }
740 
signFile(Manifest manifest, JarFile inputJar, X509Certificate[] publicKey, PrivateKey[] privateKey, JarOutputStream outputJar)741     private static void signFile(Manifest manifest, JarFile inputJar,
742                                  X509Certificate[] publicKey, PrivateKey[] privateKey,
743                                  JarOutputStream outputJar)
744         throws Exception {
745         // Assume the certificate is valid for at least an hour.
746         long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
747 
748         // MANIFEST.MF
749         JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
750         je.setTime(timestamp);
751         outputJar.putNextEntry(je);
752         manifest.write(outputJar);
753 
754         int numKeys = publicKey.length;
755         for (int k = 0; k < numKeys; ++k) {
756             // CERT.SF / CERT#.SF
757             je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
758                               (String.format(CERT_SF_MULTI_NAME, k)));
759             je.setTime(timestamp);
760             outputJar.putNextEntry(je);
761             ByteArrayOutputStream baos = new ByteArrayOutputStream();
762             writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
763             byte[] signedData = baos.toByteArray();
764             outputJar.write(signedData);
765 
766             // CERT.{EC,RSA} / CERT#.{EC,RSA}
767             final String keyType = publicKey[k].getPublicKey().getAlgorithm();
768             je = new JarEntry(numKeys == 1 ?
769                               (String.format(CERT_SIG_NAME, keyType)) :
770                               (String.format(CERT_SIG_MULTI_NAME, k, keyType)));
771             je.setTime(timestamp);
772             outputJar.putNextEntry(je);
773             writeSignatureBlock(new CMSProcessableByteArray(signedData),
774                                 publicKey[k], privateKey[k], outputJar);
775         }
776     }
777 
778     /**
779      * Tries to load a JSE Provider by class name. This is for custom PrivateKey
780      * types that might be stored in PKCS#11-like storage.
781      */
loadProviderIfNecessary(String providerClassName)782     private static void loadProviderIfNecessary(String providerClassName) {
783         if (providerClassName == null) {
784             return;
785         }
786 
787         final Class<?> klass;
788         try {
789             final ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
790             if (sysLoader != null) {
791                 klass = sysLoader.loadClass(providerClassName);
792             } else {
793                 klass = Class.forName(providerClassName);
794             }
795         } catch (ClassNotFoundException e) {
796             e.printStackTrace();
797             System.exit(1);
798             return;
799         }
800 
801         Constructor<?> constructor = null;
802         for (Constructor<?> c : klass.getConstructors()) {
803             if (c.getParameterTypes().length == 0) {
804                 constructor = c;
805                 break;
806             }
807         }
808         if (constructor == null) {
809             System.err.println("No zero-arg constructor found for " + providerClassName);
810             System.exit(1);
811             return;
812         }
813 
814         final Object o;
815         try {
816             o = constructor.newInstance();
817         } catch (Exception e) {
818             e.printStackTrace();
819             System.exit(1);
820             return;
821         }
822         if (!(o instanceof Provider)) {
823             System.err.println("Not a Provider class: " + providerClassName);
824             System.exit(1);
825         }
826 
827         Security.insertProviderAt((Provider) o, 1);
828     }
829 
usage()830     private static void usage() {
831         System.err.println("Usage: signapk [-w] " +
832                            "[-a <alignment>] " +
833                            "[-providerClass <className>] " +
834                            "publickey.x509[.pem] privatekey.pk8 " +
835                            "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
836                            "input.jar output.jar");
837         System.exit(2);
838     }
839 
main(String[] args)840     public static void main(String[] args) {
841         if (args.length < 4) usage();
842 
843         sBouncyCastleProvider = new BouncyCastleProvider();
844         Security.addProvider(sBouncyCastleProvider);
845 
846         boolean signWholeFile = false;
847         String providerClass = null;
848         String providerArg = null;
849         int alignment = 4;
850 
851         int argstart = 0;
852         while (argstart < args.length && args[argstart].startsWith("-")) {
853             if ("-w".equals(args[argstart])) {
854                 signWholeFile = true;
855                 ++argstart;
856             } else if ("-providerClass".equals(args[argstart])) {
857                 if (argstart + 1 >= args.length) {
858                     usage();
859                 }
860                 providerClass = args[++argstart];
861                 ++argstart;
862             } else if ("-a".equals(args[argstart])) {
863                 alignment = Integer.parseInt(args[++argstart]);
864                 ++argstart;
865             } else {
866                 usage();
867             }
868         }
869 
870         if ((args.length - argstart) % 2 == 1) usage();
871         int numKeys = ((args.length - argstart) / 2) - 1;
872         if (signWholeFile && numKeys > 1) {
873             System.err.println("Only one key may be used with -w.");
874             System.exit(2);
875         }
876 
877         loadProviderIfNecessary(providerClass);
878 
879         String inputFilename = args[args.length-2];
880         String outputFilename = args[args.length-1];
881 
882         JarFile inputJar = null;
883         FileOutputStream outputFile = null;
884         int hashes = 0;
885 
886         try {
887             File firstPublicKeyFile = new File(args[argstart+0]);
888 
889             X509Certificate[] publicKey = new X509Certificate[numKeys];
890             try {
891                 for (int i = 0; i < numKeys; ++i) {
892                     int argNum = argstart + i*2;
893                     publicKey[i] = readPublicKey(new File(args[argNum]));
894                     hashes |= getDigestAlgorithm(publicKey[i]);
895                 }
896             } catch (IllegalArgumentException e) {
897                 System.err.println(e);
898                 System.exit(1);
899             }
900 
901             // Set the ZIP file timestamp to the starting valid time
902             // of the 0th certificate plus one hour (to match what
903             // we've historically done).
904             long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
905 
906             PrivateKey[] privateKey = new PrivateKey[numKeys];
907             for (int i = 0; i < numKeys; ++i) {
908                 int argNum = argstart + i*2 + 1;
909                 privateKey[i] = readPrivateKey(new File(args[argNum]));
910             }
911             inputJar = new JarFile(new File(inputFilename), false);  // Don't verify.
912 
913             outputFile = new FileOutputStream(outputFilename);
914 
915 
916             if (signWholeFile) {
917                 SignApk.signWholeFile(inputJar, firstPublicKeyFile,
918                                       publicKey[0], privateKey[0], outputFile);
919             } else {
920                 JarOutputStream outputJar = new JarOutputStream(outputFile);
921 
922                 // For signing .apks, use the maximum compression to make
923                 // them as small as possible (since they live forever on
924                 // the system partition).  For OTA packages, use the
925                 // default compression level, which is much much faster
926                 // and produces output that is only a tiny bit larger
927                 // (~0.1% on full OTA packages I tested).
928                 outputJar.setLevel(9);
929 
930                 Manifest manifest = addDigestsToManifest(inputJar, hashes);
931                 copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
932                 signFile(manifest, inputJar, publicKey, privateKey, outputJar);
933                 outputJar.close();
934             }
935         } catch (Exception e) {
936             e.printStackTrace();
937             System.exit(1);
938         } finally {
939             try {
940                 if (inputJar != null) inputJar.close();
941                 if (outputFile != null) outputFile.close();
942             } catch (IOException e) {
943                 e.printStackTrace();
944                 System.exit(1);
945             }
946         }
947     }
948 }
949