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.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.conscrypt.OpenSSLProvider;
36 
37 import com.android.apksig.ApkSignerEngine;
38 import com.android.apksig.DefaultApkSignerEngine;
39 import com.android.apksig.apk.ApkUtils;
40 import com.android.apksig.apk.MinSdkVersionException;
41 import com.android.apksig.util.DataSink;
42 import com.android.apksig.util.DataSources;
43 import com.android.apksig.zip.ZipFormatException;
44 
45 import java.io.Console;
46 import java.io.BufferedReader;
47 import java.io.ByteArrayInputStream;
48 import java.io.ByteArrayOutputStream;
49 import java.io.DataInputStream;
50 import java.io.File;
51 import java.io.FileInputStream;
52 import java.io.FileOutputStream;
53 import java.io.FilterOutputStream;
54 import java.io.IOException;
55 import java.io.InputStream;
56 import java.io.InputStreamReader;
57 import java.io.OutputStream;
58 import java.lang.reflect.Constructor;
59 import java.nio.ByteBuffer;
60 import java.nio.ByteOrder;
61 import java.nio.charset.StandardCharsets;
62 import java.security.GeneralSecurityException;
63 import java.security.Key;
64 import java.security.KeyFactory;
65 import java.security.PrivateKey;
66 import java.security.Provider;
67 import java.security.Security;
68 import java.security.cert.CertificateEncodingException;
69 import java.security.cert.CertificateFactory;
70 import java.security.cert.X509Certificate;
71 import java.security.spec.InvalidKeySpecException;
72 import java.security.spec.PKCS8EncodedKeySpec;
73 import java.util.ArrayList;
74 import java.util.Collections;
75 import java.util.Enumeration;
76 import java.util.List;
77 import java.util.Locale;
78 import java.util.TimeZone;
79 import java.util.jar.JarEntry;
80 import java.util.jar.JarFile;
81 import java.util.jar.JarOutputStream;
82 import java.util.regex.Pattern;
83 
84 import javax.crypto.Cipher;
85 import javax.crypto.EncryptedPrivateKeyInfo;
86 import javax.crypto.SecretKeyFactory;
87 import javax.crypto.spec.PBEKeySpec;
88 
89 /**
90  * HISTORICAL NOTE:
91  *
92  * Prior to the keylimepie release, SignApk ignored the signature
93  * algorithm specified in the certificate and always used SHA1withRSA.
94  *
95  * Starting with JB-MR2, the platform supports SHA256withRSA, so we use
96  * the signature algorithm in the certificate to select which to use
97  * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported.
98  *
99  * Because there are old keys still in use whose certificate actually
100  * says "MD5withRSA", we treat these as though they say "SHA1withRSA"
101  * for compatibility with older releases.  This can be changed by
102  * altering the getAlgorithm() function below.
103  */
104 
105 
106 /**
107  * Command line tool to sign JAR files (including APKs and OTA updates) in a way
108  * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or
109  * SHA-256 (see historical note). The tool can additionally sign APKs using
110  * APK Signature Scheme v2.
111  */
112 class SignApk {
113     private static final String OTACERT_NAME = "META-INF/com/android/otacert";
114 
115     /**
116      * Extensible data block/field header ID used for storing information about alignment of
117      * uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section
118      * 4.5 Extensible data fields.
119      */
120     private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935;
121 
122     /**
123      * Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed
124      * entries.
125      */
126     private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6;
127 
128     // bitmasks for which hash algorithms we need the manifest to include.
129     private static final int USE_SHA1 = 1;
130     private static final int USE_SHA256 = 2;
131 
132     /**
133      * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used
134      * for signing an OTA update package using the private key corresponding to the provided
135      * certificate.
136      */
getDigestAlgorithmForOta(X509Certificate cert)137     private static int getDigestAlgorithmForOta(X509Certificate cert) {
138         String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
139         if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) {
140             // see "HISTORICAL NOTE" above.
141             return USE_SHA1;
142         } else if (sigAlg.startsWith("SHA256WITH")) {
143             return USE_SHA256;
144         } else {
145             throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
146                                                "\" in cert [" + cert.getSubjectDN());
147         }
148     }
149 
150     /**
151      * Returns the JCA {@link java.security.Signature} algorithm to be used for signing and OTA
152      * update package using the private key corresponding to the provided certificate and the
153      * provided digest algorithm (see {@code USE_SHA1} and {@code USE_SHA256} constants).
154      */
getJcaSignatureAlgorithmForOta( X509Certificate cert, int hash)155     private static String getJcaSignatureAlgorithmForOta(
156             X509Certificate cert, int hash) {
157         String sigAlgDigestPrefix;
158         switch (hash) {
159             case USE_SHA1:
160                 sigAlgDigestPrefix = "SHA1";
161                 break;
162             case USE_SHA256:
163                 sigAlgDigestPrefix = "SHA256";
164                 break;
165             default:
166                 throw new IllegalArgumentException("Unknown hash ID: " + hash);
167         }
168 
169         String keyAlgorithm = cert.getPublicKey().getAlgorithm();
170         if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
171             return sigAlgDigestPrefix + "withRSA";
172         } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
173             return sigAlgDigestPrefix + "withECDSA";
174         } else {
175             throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
176         }
177     }
178 
readPublicKey(File file)179     private static X509Certificate readPublicKey(File file)
180         throws IOException, GeneralSecurityException {
181         FileInputStream input = new FileInputStream(file);
182         try {
183             CertificateFactory cf = CertificateFactory.getInstance("X.509");
184             return (X509Certificate) cf.generateCertificate(input);
185         } finally {
186             input.close();
187         }
188     }
189 
190     /**
191      * If a console doesn't exist, reads the password from stdin
192      * If a console exists, reads the password from console and returns it as a string.
193      *
194      * @param keyFile The file containing the private key.  Used to prompt the user.
195      */
readPassword(File keyFile)196     private static String readPassword(File keyFile) {
197         Console console;
198         char[] pwd;
199         if ((console = System.console()) == null) {
200             System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
201             System.out.flush();
202             BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
203             try {
204                 return stdin.readLine();
205             } catch (IOException ex) {
206                 return null;
207             }
208         } else {
209             if ((pwd = console.readPassword("[%s]", "Enter password for " + keyFile)) != null) {
210                 return String.valueOf(pwd);
211             } else {
212                 return null;
213             }
214         }
215     }
216 
217     /**
218      * Decrypt an encrypted PKCS#8 format private key.
219      *
220      * Based on ghstark's post on Aug 6, 2006 at
221      * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
222      *
223      * @param encryptedPrivateKey The raw data of the private key
224      * @param keyFile The file containing the private key
225      */
decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)226     private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
227         throws GeneralSecurityException {
228         EncryptedPrivateKeyInfo epkInfo;
229         try {
230             epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
231         } catch (IOException ex) {
232             // Probably not an encrypted key.
233             return null;
234         }
235 
236         char[] password = readPassword(keyFile).toCharArray();
237 
238         SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
239         Key key = skFactory.generateSecret(new PBEKeySpec(password));
240 
241         Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
242         cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
243 
244         try {
245             return epkInfo.getKeySpec(cipher);
246         } catch (InvalidKeySpecException ex) {
247             System.err.println("signapk: Password for " + keyFile + " may be bad.");
248             throw ex;
249         }
250     }
251 
252     /** Read a PKCS#8 format private key. */
readPrivateKey(File file)253     private static PrivateKey readPrivateKey(File file)
254         throws IOException, GeneralSecurityException {
255         DataInputStream input = new DataInputStream(new FileInputStream(file));
256         try {
257             byte[] bytes = new byte[(int) file.length()];
258             input.read(bytes);
259 
260             /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
261             PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file);
262             if (spec == null) {
263                 spec = new PKCS8EncodedKeySpec(bytes);
264             }
265 
266             /*
267              * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
268              * OID and use that to construct a KeyFactory.
269              */
270             PrivateKeyInfo pki;
271             try (ASN1InputStream bIn =
272                     new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()))) {
273                 pki = PrivateKeyInfo.getInstance(bIn.readObject());
274             }
275             String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
276 
277             return KeyFactory.getInstance(algOid).generatePrivate(spec);
278         } finally {
279             input.close();
280         }
281     }
282 
283     /**
284      * Add a copy of the public key to the archive; this should
285      * exactly match one of the files in
286      * /system/etc/security/otacerts.zip on the device.  (The same
287      * cert can be extracted from the OTA update package's signature
288      * block but this is much easier to get at.)
289      */
addOtacert(JarOutputStream outputJar, File publicKeyFile, long timestamp)290     private static void addOtacert(JarOutputStream outputJar,
291                                    File publicKeyFile,
292                                    long timestamp)
293         throws IOException {
294 
295         JarEntry je = new JarEntry(OTACERT_NAME);
296         je.setTime(timestamp);
297         outputJar.putNextEntry(je);
298         FileInputStream input = new FileInputStream(publicKeyFile);
299         byte[] b = new byte[4096];
300         int read;
301         while ((read = input.read(b)) != -1) {
302             outputJar.write(b, 0, read);
303         }
304         input.close();
305     }
306 
307 
308     /** Sign data and write the digital signature to 'out'. */
writeSignatureBlock( CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash, OutputStream out)309     private static void writeSignatureBlock(
310         CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash,
311         OutputStream out)
312         throws IOException,
313                CertificateEncodingException,
314                OperatorCreationException,
315                CMSException {
316         ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
317         certList.add(publicKey);
318         JcaCertStore certs = new JcaCertStore(certList);
319 
320         CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
321         ContentSigner signer =
322                 new JcaContentSignerBuilder(
323                         getJcaSignatureAlgorithmForOta(publicKey, hash))
324                         .build(privateKey);
325         gen.addSignerInfoGenerator(
326             new JcaSignerInfoGeneratorBuilder(
327                 new JcaDigestCalculatorProviderBuilder()
328                 .build())
329             .setDirectSignature(true)
330             .build(signer, publicKey));
331         gen.addCertificates(certs);
332         CMSSignedData sigData = gen.generate(data, false);
333 
334         try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
335             DEROutputStream dos = new DEROutputStream(out);
336             dos.writeObject(asn1.readObject());
337         }
338     }
339 
340     /**
341      * Adds ZIP entries which represent the v1 signature (JAR signature scheme).
342      */
addV1Signature( ApkSignerEngine apkSigner, ApkSignerEngine.OutputJarSignatureRequest v1Signature, JarOutputStream out, long timestamp)343     private static void addV1Signature(
344             ApkSignerEngine apkSigner,
345             ApkSignerEngine.OutputJarSignatureRequest v1Signature,
346             JarOutputStream out,
347             long timestamp) throws IOException {
348         for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry
349                 : v1Signature.getAdditionalJarEntries()) {
350             String entryName = entry.getName();
351             JarEntry outEntry = new JarEntry(entryName);
352             outEntry.setTime(timestamp);
353             out.putNextEntry(outEntry);
354             byte[] entryData = entry.getData();
355             out.write(entryData);
356             ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
357                     apkSigner.outputJarEntry(entryName);
358             if (inspectEntryRequest != null) {
359                 inspectEntryRequest.getDataSink().consume(entryData, 0, entryData.length);
360                 inspectEntryRequest.done();
361             }
362         }
363     }
364 
365     /**
366      * Copy all JAR entries from input to output. We set the modification times in the output to a
367      * fixed time, so as to reduce variation in the output file and make incremental OTAs more
368      * efficient.
369      */
copyFiles( JarFile in, Pattern ignoredFilenamePattern, ApkSignerEngine apkSigner, JarOutputStream out, long timestamp, int defaultAlignment)370     private static void copyFiles(
371             JarFile in,
372             Pattern ignoredFilenamePattern,
373             ApkSignerEngine apkSigner,
374             JarOutputStream out,
375             long timestamp,
376             int defaultAlignment) throws IOException {
377         byte[] buffer = new byte[4096];
378         int num;
379 
380         ArrayList<String> names = new ArrayList<String>();
381         for (Enumeration<JarEntry> e = in.entries(); e.hasMoreElements();) {
382             JarEntry entry = e.nextElement();
383             if (entry.isDirectory()) {
384                 continue;
385             }
386             String entryName = entry.getName();
387             if ((ignoredFilenamePattern != null)
388                     && (ignoredFilenamePattern.matcher(entryName).matches())) {
389                 continue;
390             }
391             names.add(entryName);
392         }
393         Collections.sort(names);
394 
395         boolean firstEntry = true;
396         long offset = 0L;
397 
398         // We do the copy in two passes -- first copying all the
399         // entries that are STORED, then copying all the entries that
400         // have any other compression flag (which in practice means
401         // DEFLATED).  This groups all the stored entries together at
402         // the start of the file and makes it easier to do alignment
403         // on them (since only stored entries are aligned).
404 
405         List<String> remainingNames = new ArrayList<>(names.size());
406         for (String name : names) {
407             JarEntry inEntry = in.getJarEntry(name);
408             if (inEntry.getMethod() != JarEntry.STORED) {
409                 // Defer outputting this entry until we're ready to output compressed entries.
410                 remainingNames.add(name);
411                 continue;
412             }
413 
414             if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) {
415                 continue;
416             }
417 
418             // Preserve the STORED method of the input entry.
419             JarEntry outEntry = new JarEntry(inEntry);
420             outEntry.setTime(timestamp);
421             // Discard comment and extra fields of this entry to
422             // simplify alignment logic below and for consistency with
423             // how compressed entries are handled later.
424             outEntry.setComment(null);
425             outEntry.setExtra(null);
426 
427             int alignment = getStoredEntryDataAlignment(name, defaultAlignment);
428             // Alignment of the entry's data is achieved by adding a data block to the entry's Local
429             // File Header extra field. The data block contains information about the alignment
430             // value and the necessary padding bytes (0x00) to achieve the alignment.  This works
431             // because the entry's data will be located immediately after the extra field.
432             // See ZIP APPNOTE.txt section "4.5 Extensible data fields" for details about the format
433             // of the extra field.
434 
435             // 'offset' is the offset into the file at which we expect the entry's data to begin.
436             // This is the value we need to make a multiple of 'alignment'.
437             offset += JarFile.LOCHDR + outEntry.getName().length();
438             if (firstEntry) {
439                 // The first entry in a jar file has an extra field of four bytes that you can't get
440                 // rid of; any extra data you specify in the JarEntry is appended to these forced
441                 // four bytes.  This is JAR_MAGIC in JarOutputStream; the bytes are 0xfeca0000.
442                 // See http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6808540
443                 // and http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4138619.
444                 offset += 4;
445                 firstEntry = false;
446             }
447             int extraPaddingSizeBytes = 0;
448             if (alignment > 0) {
449                 long paddingStartOffset = offset + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES;
450                 extraPaddingSizeBytes =
451                         (alignment - (int) (paddingStartOffset % alignment)) % alignment;
452             }
453             byte[] extra =
454                     new byte[ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES + extraPaddingSizeBytes];
455             ByteBuffer extraBuf = ByteBuffer.wrap(extra);
456             extraBuf.order(ByteOrder.LITTLE_ENDIAN);
457             extraBuf.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); // Header ID
458             extraBuf.putShort((short) (2 + extraPaddingSizeBytes)); // Data Size
459             extraBuf.putShort((short) alignment);
460             outEntry.setExtra(extra);
461             offset += extra.length;
462 
463             out.putNextEntry(outEntry);
464             ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
465                     (apkSigner != null) ? apkSigner.outputJarEntry(name) : null;
466             DataSink entryDataSink =
467                     (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null;
468 
469             try (InputStream data = in.getInputStream(inEntry)) {
470                 while ((num = data.read(buffer)) > 0) {
471                     out.write(buffer, 0, num);
472                     if (entryDataSink != null) {
473                         entryDataSink.consume(buffer, 0, num);
474                     }
475                     offset += num;
476                 }
477             }
478             out.flush();
479             if (inspectEntryRequest != null) {
480                 inspectEntryRequest.done();
481             }
482         }
483 
484         // Copy all the non-STORED entries.  We don't attempt to
485         // maintain the 'offset' variable past this point; we don't do
486         // alignment on these entries.
487 
488         for (String name : remainingNames) {
489             JarEntry inEntry = in.getJarEntry(name);
490             if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) {
491                 continue;
492             }
493 
494             // Create a new entry so that the compressed len is recomputed.
495             JarEntry outEntry = new JarEntry(name);
496             outEntry.setTime(timestamp);
497             out.putNextEntry(outEntry);
498             ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
499                     (apkSigner != null) ? apkSigner.outputJarEntry(name) : null;
500             DataSink entryDataSink =
501                     (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null;
502 
503             InputStream data = in.getInputStream(inEntry);
504             while ((num = data.read(buffer)) > 0) {
505                 out.write(buffer, 0, num);
506                 if (entryDataSink != null) {
507                     entryDataSink.consume(buffer, 0, num);
508                 }
509             }
510             out.flush();
511             if (inspectEntryRequest != null) {
512                 inspectEntryRequest.done();
513             }
514         }
515     }
516 
shouldOutputApkEntry( ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf)517     private static boolean shouldOutputApkEntry(
518             ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf)
519                     throws IOException {
520         if (apkSigner == null) {
521             return true;
522         }
523 
524         ApkSignerEngine.InputJarEntryInstructions instructions =
525                 apkSigner.inputJarEntry(inEntry.getName());
526         ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
527                 instructions.getInspectJarEntryRequest();
528         if (inspectEntryRequest != null) {
529             provideJarEntry(inFile, inEntry, inspectEntryRequest, tmpbuf);
530         }
531         switch (instructions.getOutputPolicy()) {
532             case OUTPUT:
533                 return true;
534             case SKIP:
535             case OUTPUT_BY_ENGINE:
536                 return false;
537             default:
538                 throw new RuntimeException(
539                         "Unsupported output policy: " + instructions.getOutputPolicy());
540         }
541     }
542 
provideJarEntry( JarFile jarFile, JarEntry jarEntry, ApkSignerEngine.InspectJarEntryRequest request, byte[] tmpbuf)543     private static void provideJarEntry(
544             JarFile jarFile,
545             JarEntry jarEntry,
546             ApkSignerEngine.InspectJarEntryRequest request,
547             byte[] tmpbuf) throws IOException {
548         DataSink dataSink = request.getDataSink();
549         try (InputStream in = jarFile.getInputStream(jarEntry)) {
550             int chunkSize;
551             while ((chunkSize = in.read(tmpbuf)) > 0) {
552                 dataSink.consume(tmpbuf, 0, chunkSize);
553             }
554             request.done();
555         }
556     }
557 
558     /**
559      * Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start
560      * relative to start of file or {@code 0} if alignment of this entry's data is not important.
561      */
getStoredEntryDataAlignment(String entryName, int defaultAlignment)562     private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment) {
563         if (defaultAlignment <= 0) {
564             return 0;
565         }
566 
567         if (entryName.endsWith(".so")) {
568             // Align .so contents to memory page boundary to enable memory-mapped
569             // execution.
570             return 4096;
571         } else {
572             return defaultAlignment;
573         }
574     }
575 
576     private static class WholeFileSignerOutputStream extends FilterOutputStream {
577         private boolean closing = false;
578         private ByteArrayOutputStream footer = new ByteArrayOutputStream();
579         private OutputStream tee;
580 
WholeFileSignerOutputStream(OutputStream out, OutputStream tee)581         public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
582             super(out);
583             this.tee = tee;
584         }
585 
notifyClosing()586         public void notifyClosing() {
587             closing = true;
588         }
589 
finish()590         public void finish() throws IOException {
591             closing = false;
592 
593             byte[] data = footer.toByteArray();
594             if (data.length < 2)
595                 throw new IOException("Less than two bytes written to footer");
596             write(data, 0, data.length - 2);
597         }
598 
getTail()599         public byte[] getTail() {
600             return footer.toByteArray();
601         }
602 
603         @Override
write(byte[] b)604         public void write(byte[] b) throws IOException {
605             write(b, 0, b.length);
606         }
607 
608         @Override
write(byte[] b, int off, int len)609         public void write(byte[] b, int off, int len) throws IOException {
610             if (closing) {
611                 // if the jar is about to close, save the footer that will be written
612                 footer.write(b, off, len);
613             }
614             else {
615                 // write to both output streams. out is the CMSTypedData signer and tee is the file.
616                 out.write(b, off, len);
617                 tee.write(b, off, len);
618             }
619         }
620 
621         @Override
write(int b)622         public void write(int b) throws IOException {
623             if (closing) {
624                 // if the jar is about to close, save the footer that will be written
625                 footer.write(b);
626             }
627             else {
628                 // write to both output streams. out is the CMSTypedData signer and tee is the file.
629                 out.write(b);
630                 tee.write(b);
631             }
632         }
633     }
634 
635     private static class CMSSigner implements CMSTypedData {
636         private final JarFile inputJar;
637         private final File publicKeyFile;
638         private final X509Certificate publicKey;
639         private final PrivateKey privateKey;
640         private final int hash;
641         private final long timestamp;
642         private final OutputStream outputStream;
643         private final ASN1ObjectIdentifier type;
644         private WholeFileSignerOutputStream signer;
645 
646         // Files matching this pattern are not copied to the output.
647         private static final Pattern STRIP_PATTERN =
648                 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|("
649                         + Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
650 
CMSSigner(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, int hash, long timestamp, OutputStream outputStream)651         public CMSSigner(JarFile inputJar, File publicKeyFile,
652                          X509Certificate publicKey, PrivateKey privateKey, int hash,
653                          long timestamp, OutputStream outputStream) {
654             this.inputJar = inputJar;
655             this.publicKeyFile = publicKeyFile;
656             this.publicKey = publicKey;
657             this.privateKey = privateKey;
658             this.hash = hash;
659             this.timestamp = timestamp;
660             this.outputStream = outputStream;
661             this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
662         }
663 
664         /**
665          * This should actually return byte[] or something similar, but nothing
666          * actually checks it currently.
667          */
668         @Override
getContent()669         public Object getContent() {
670             return this;
671         }
672 
673         @Override
getContentType()674         public ASN1ObjectIdentifier getContentType() {
675             return type;
676         }
677 
678         @Override
write(OutputStream out)679         public void write(OutputStream out) throws IOException {
680             try {
681                 signer = new WholeFileSignerOutputStream(out, outputStream);
682                 JarOutputStream outputJar = new JarOutputStream(signer);
683 
684                 copyFiles(inputJar, STRIP_PATTERN, null, outputJar, timestamp, 0);
685                 addOtacert(outputJar, publicKeyFile, timestamp);
686 
687                 signer.notifyClosing();
688                 outputJar.close();
689                 signer.finish();
690             }
691             catch (Exception e) {
692                 throw new IOException(e);
693             }
694         }
695 
writeSignatureBlock(ByteArrayOutputStream temp)696         public void writeSignatureBlock(ByteArrayOutputStream temp)
697             throws IOException,
698                    CertificateEncodingException,
699                    OperatorCreationException,
700                    CMSException {
701             SignApk.writeSignatureBlock(this, publicKey, privateKey, hash, temp);
702         }
703 
getSigner()704         public WholeFileSignerOutputStream getSigner() {
705             return signer;
706         }
707     }
708 
signWholeFile(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, int hash, long timestamp, OutputStream outputStream)709     private static void signWholeFile(JarFile inputJar, File publicKeyFile,
710                                       X509Certificate publicKey, PrivateKey privateKey,
711                                       int hash, long timestamp,
712                                       OutputStream outputStream) throws Exception {
713         CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
714                 publicKey, privateKey, hash, timestamp, outputStream);
715 
716         ByteArrayOutputStream temp = new ByteArrayOutputStream();
717 
718         // put a readable message and a null char at the start of the
719         // archive comment, so that tools that display the comment
720         // (hopefully) show something sensible.
721         // TODO: anything more useful we can put in this message?
722         byte[] message = "signed by SignApk".getBytes(StandardCharsets.UTF_8);
723         temp.write(message);
724         temp.write(0);
725 
726         cmsOut.writeSignatureBlock(temp);
727 
728         byte[] zipData = cmsOut.getSigner().getTail();
729 
730         // For a zip with no archive comment, the
731         // end-of-central-directory record will be 22 bytes long, so
732         // we expect to find the EOCD marker 22 bytes from the end.
733         if (zipData[zipData.length-22] != 0x50 ||
734             zipData[zipData.length-21] != 0x4b ||
735             zipData[zipData.length-20] != 0x05 ||
736             zipData[zipData.length-19] != 0x06) {
737             throw new IllegalArgumentException("zip data already has an archive comment");
738         }
739 
740         int total_size = temp.size() + 6;
741         if (total_size > 0xffff) {
742             throw new IllegalArgumentException("signature is too big for ZIP file comment");
743         }
744         // signature starts this many bytes from the end of the file
745         int signature_start = total_size - message.length - 1;
746         temp.write(signature_start & 0xff);
747         temp.write((signature_start >> 8) & 0xff);
748         // Why the 0xff bytes?  In a zip file with no archive comment,
749         // bytes [-6:-2] of the file are the little-endian offset from
750         // the start of the file to the central directory.  So for the
751         // two high bytes to be 0xff 0xff, the archive would have to
752         // be nearly 4GB in size.  So it's unlikely that a real
753         // commentless archive would have 0xffs here, and lets us tell
754         // an old signed archive from a new one.
755         temp.write(0xff);
756         temp.write(0xff);
757         temp.write(total_size & 0xff);
758         temp.write((total_size >> 8) & 0xff);
759         temp.flush();
760 
761         // Signature verification checks that the EOCD header is the
762         // last such sequence in the file (to avoid minzip finding a
763         // fake EOCD appended after the signature in its scan).  The
764         // odds of producing this sequence by chance are very low, but
765         // let's catch it here if it does.
766         byte[] b = temp.toByteArray();
767         for (int i = 0; i < b.length-3; ++i) {
768             if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
769                 throw new IllegalArgumentException("found spurious EOCD header at " + i);
770             }
771         }
772 
773         outputStream.write(total_size & 0xff);
774         outputStream.write((total_size >> 8) & 0xff);
775         temp.writeTo(outputStream);
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 
createSignerConfigs( PrivateKey[] privateKeys, X509Certificate[] certificates)830     private static List<DefaultApkSignerEngine.SignerConfig> createSignerConfigs(
831             PrivateKey[] privateKeys, X509Certificate[] certificates) {
832         if (privateKeys.length != certificates.length) {
833             throw new IllegalArgumentException(
834                     "The number of private keys must match the number of certificates: "
835                             + privateKeys.length + " vs" + certificates.length);
836         }
837         List<DefaultApkSignerEngine.SignerConfig> signerConfigs = new ArrayList<>();
838         String signerNameFormat = (privateKeys.length == 1) ? "CERT" : "CERT%s";
839         for (int i = 0; i < privateKeys.length; i++) {
840             String signerName = String.format(Locale.US, signerNameFormat, (i + 1));
841             DefaultApkSignerEngine.SignerConfig signerConfig =
842                     new DefaultApkSignerEngine.SignerConfig.Builder(
843                             signerName,
844                             privateKeys[i],
845                             Collections.singletonList(certificates[i]))
846                             .build();
847             signerConfigs.add(signerConfig);
848         }
849         return signerConfigs;
850     }
851 
852     private static class ZipSections {
853         ByteBuffer beforeCentralDir;
854         ByteBuffer centralDir;
855         ByteBuffer eocd;
856     }
857 
findMainZipSections(ByteBuffer apk)858     private static ZipSections findMainZipSections(ByteBuffer apk)
859             throws IOException, ZipFormatException {
860         apk.slice();
861         ApkUtils.ZipSections sections = ApkUtils.findZipSections(DataSources.asDataSource(apk));
862         long centralDirStartOffset = sections.getZipCentralDirectoryOffset();
863         long centralDirSizeBytes = sections.getZipCentralDirectorySizeBytes();
864         long centralDirEndOffset = centralDirStartOffset + centralDirSizeBytes;
865         long eocdStartOffset = sections.getZipEndOfCentralDirectoryOffset();
866         if (centralDirEndOffset != eocdStartOffset) {
867             throw new ZipFormatException(
868                     "ZIP Central Directory is not immediately followed by End of Central Directory"
869                             + ". CD end: " + centralDirEndOffset
870                             + ", EoCD start: " + eocdStartOffset);
871         }
872         apk.position(0);
873         apk.limit((int) centralDirStartOffset);
874         ByteBuffer beforeCentralDir = apk.slice();
875 
876         apk.position((int) centralDirStartOffset);
877         apk.limit((int) centralDirEndOffset);
878         ByteBuffer centralDir = apk.slice();
879 
880         apk.position((int) eocdStartOffset);
881         apk.limit(apk.capacity());
882         ByteBuffer eocd = apk.slice();
883 
884         apk.position(0);
885         apk.limit(apk.capacity());
886 
887         ZipSections result = new ZipSections();
888         result.beforeCentralDir = beforeCentralDir;
889         result.centralDir = centralDir;
890         result.eocd = eocd;
891         return result;
892     }
893 
894     /**
895      * Returns the API Level corresponding to the APK's minSdkVersion.
896      *
897      * @throws MinSdkVersionException if the API Level cannot be determined from the APK.
898      */
getMinSdkVersion(JarFile apk)899     private static final int getMinSdkVersion(JarFile apk) throws MinSdkVersionException {
900         JarEntry manifestEntry = apk.getJarEntry("AndroidManifest.xml");
901         if (manifestEntry == null) {
902             throw new MinSdkVersionException("No AndroidManifest.xml in APK");
903         }
904         byte[] manifestBytes;
905         try {
906             try (InputStream manifestIn = apk.getInputStream(manifestEntry)) {
907                 manifestBytes = toByteArray(manifestIn);
908             }
909         } catch (IOException e) {
910             throw new MinSdkVersionException("Failed to read AndroidManifest.xml", e);
911         }
912         return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(ByteBuffer.wrap(manifestBytes));
913     }
914 
toByteArray(InputStream in)915     private static byte[] toByteArray(InputStream in) throws IOException {
916         ByteArrayOutputStream result = new ByteArrayOutputStream();
917         byte[] buf = new byte[65536];
918         int chunkSize;
919         while ((chunkSize = in.read(buf)) != -1) {
920             result.write(buf, 0, chunkSize);
921         }
922         return result.toByteArray();
923     }
924 
usage()925     private static void usage() {
926         System.err.println("Usage: signapk [-w] " +
927                            "[-a <alignment>] " +
928                            "[-providerClass <className>] " +
929                            "[--min-sdk-version <n>] " +
930                            "[--disable-v2] " +
931                            "publickey.x509[.pem] privatekey.pk8 " +
932                            "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
933                            "input.jar output.jar");
934         System.exit(2);
935     }
936 
main(String[] args)937     public static void main(String[] args) {
938         if (args.length < 4) usage();
939 
940         // Install Conscrypt as the highest-priority provider. Its crypto primitives are faster than
941         // the standard or Bouncy Castle ones.
942         Security.insertProviderAt(new OpenSSLProvider(), 1);
943         // Install Bouncy Castle (as the lowest-priority provider) because Conscrypt does not offer
944         // DSA which may still be needed.
945         // TODO: Stop installing Bouncy Castle provider once DSA is no longer needed.
946         Security.addProvider(new BouncyCastleProvider());
947 
948         boolean signWholeFile = false;
949         String providerClass = null;
950         int alignment = 4;
951         Integer minSdkVersionOverride = null;
952         boolean signUsingApkSignatureSchemeV2 = true;
953 
954         int argstart = 0;
955         while (argstart < args.length && args[argstart].startsWith("-")) {
956             if ("-w".equals(args[argstart])) {
957                 signWholeFile = true;
958                 ++argstart;
959             } else if ("-providerClass".equals(args[argstart])) {
960                 if (argstart + 1 >= args.length) {
961                     usage();
962                 }
963                 providerClass = args[++argstart];
964                 ++argstart;
965             } else if ("-a".equals(args[argstart])) {
966                 alignment = Integer.parseInt(args[++argstart]);
967                 ++argstart;
968             } else if ("--min-sdk-version".equals(args[argstart])) {
969                 String minSdkVersionString = args[++argstart];
970                 try {
971                     minSdkVersionOverride = Integer.parseInt(minSdkVersionString);
972                 } catch (NumberFormatException e) {
973                     throw new IllegalArgumentException(
974                             "--min-sdk-version must be a decimal number: " + minSdkVersionString);
975                 }
976                 ++argstart;
977             } else if ("--disable-v2".equals(args[argstart])) {
978                 signUsingApkSignatureSchemeV2 = false;
979                 ++argstart;
980             } else {
981                 usage();
982             }
983         }
984 
985         if ((args.length - argstart) % 2 == 1) usage();
986         int numKeys = ((args.length - argstart) / 2) - 1;
987         if (signWholeFile && numKeys > 1) {
988             System.err.println("Only one key may be used with -w.");
989             System.exit(2);
990         }
991 
992         loadProviderIfNecessary(providerClass);
993 
994         String inputFilename = args[args.length-2];
995         String outputFilename = args[args.length-1];
996 
997         JarFile inputJar = null;
998         FileOutputStream outputFile = null;
999 
1000         try {
1001             File firstPublicKeyFile = new File(args[argstart+0]);
1002 
1003             X509Certificate[] publicKey = new X509Certificate[numKeys];
1004             try {
1005                 for (int i = 0; i < numKeys; ++i) {
1006                     int argNum = argstart + i*2;
1007                     publicKey[i] = readPublicKey(new File(args[argNum]));
1008                 }
1009             } catch (IllegalArgumentException e) {
1010                 System.err.println(e);
1011                 System.exit(1);
1012             }
1013 
1014             // Set all ZIP file timestamps to Jan 1 2009 00:00:00.
1015             long timestamp = 1230768000000L;
1016             // The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS
1017             // timestamp using the current timezone. We thus adjust the milliseconds since epoch
1018             // value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00.
1019             timestamp -= TimeZone.getDefault().getOffset(timestamp);
1020 
1021             PrivateKey[] privateKey = new PrivateKey[numKeys];
1022             for (int i = 0; i < numKeys; ++i) {
1023                 int argNum = argstart + i*2 + 1;
1024                 privateKey[i] = readPrivateKey(new File(args[argNum]));
1025             }
1026             inputJar = new JarFile(new File(inputFilename), false);  // Don't verify.
1027 
1028             outputFile = new FileOutputStream(outputFilename);
1029 
1030             // NOTE: Signing currently recompresses any compressed entries using Deflate (default
1031             // compression level for OTA update files and maximum compession level for APKs).
1032             if (signWholeFile) {
1033                 int digestAlgorithm = getDigestAlgorithmForOta(publicKey[0]);
1034                 signWholeFile(inputJar, firstPublicKeyFile,
1035                         publicKey[0], privateKey[0], digestAlgorithm,
1036                         timestamp,
1037                         outputFile);
1038             } else {
1039                 // Determine the value to use as minSdkVersion of the APK being signed
1040                 int minSdkVersion;
1041                 if (minSdkVersionOverride != null) {
1042                     minSdkVersion = minSdkVersionOverride;
1043                 } else {
1044                     try {
1045                         minSdkVersion = getMinSdkVersion(inputJar);
1046                     } catch (MinSdkVersionException e) {
1047                         throw new IllegalArgumentException(
1048                                 "Cannot detect minSdkVersion. Use --min-sdk-version to override",
1049                                 e);
1050                     }
1051                 }
1052 
1053                 try (ApkSignerEngine apkSigner =
1054                         new DefaultApkSignerEngine.Builder(
1055                                 createSignerConfigs(privateKey, publicKey), minSdkVersion)
1056                                 .setV1SigningEnabled(true)
1057                                 .setV2SigningEnabled(signUsingApkSignatureSchemeV2)
1058                                 .setOtherSignersSignaturesPreserved(false)
1059                                 .setCreatedBy("1.0 (Android SignApk)")
1060                                 .build()) {
1061                     // We don't preserve the input APK's APK Signing Block (which contains v2
1062                     // signatures)
1063                     apkSigner.inputApkSigningBlock(null);
1064 
1065                     // Build the output APK in memory, by copying input APK's ZIP entries across
1066                     // and then signing the output APK.
1067                     ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
1068                     JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf);
1069                     // Use maximum compression for compressed entries because the APK lives forever
1070                     // on the system partition.
1071                     outputJar.setLevel(9);
1072                     copyFiles(inputJar, null, apkSigner, outputJar, timestamp, alignment);
1073                     ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest =
1074                             apkSigner.outputJarEntries();
1075                     if (addV1SignatureRequest != null) {
1076                         addV1Signature(apkSigner, addV1SignatureRequest, outputJar, timestamp);
1077                         addV1SignatureRequest.done();
1078                     }
1079                     outputJar.close();
1080                     ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
1081                     v1SignedApkBuf.reset();
1082                     ByteBuffer[] outputChunks = new ByteBuffer[] {v1SignedApk};
1083 
1084                     ZipSections zipSections = findMainZipSections(v1SignedApk);
1085                     ApkSignerEngine.OutputApkSigningBlockRequest2 addV2SignatureRequest =
1086                             apkSigner.outputZipSections2(
1087                                     DataSources.asDataSource(zipSections.beforeCentralDir),
1088                                     DataSources.asDataSource(zipSections.centralDir),
1089                                     DataSources.asDataSource(zipSections.eocd));
1090                     if (addV2SignatureRequest != null) {
1091                         // Need to insert the returned APK Signing Block before ZIP Central
1092                         // Directory.
1093                         int padding = addV2SignatureRequest.getPaddingSizeBeforeApkSigningBlock();
1094                         byte[] apkSigningBlock = addV2SignatureRequest.getApkSigningBlock();
1095                         // Because the APK Signing Block is inserted before the Central Directory,
1096                         // we need to adjust accordingly the offset of Central Directory inside the
1097                         // ZIP End of Central Directory (EoCD) record.
1098                         ByteBuffer modifiedEocd = ByteBuffer.allocate(zipSections.eocd.remaining());
1099                         modifiedEocd.put(zipSections.eocd);
1100                         modifiedEocd.flip();
1101                         modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
1102                         ApkUtils.setZipEocdCentralDirectoryOffset(
1103                                 modifiedEocd,
1104                                 zipSections.beforeCentralDir.remaining() + padding +
1105                                 apkSigningBlock.length);
1106                         outputChunks =
1107                                 new ByteBuffer[] {
1108                                         zipSections.beforeCentralDir,
1109                                         ByteBuffer.allocate(padding),
1110                                         ByteBuffer.wrap(apkSigningBlock),
1111                                         zipSections.centralDir,
1112                                         modifiedEocd};
1113                         addV2SignatureRequest.done();
1114                     }
1115 
1116                     // This assumes outputChunks are array-backed. To avoid this assumption, the
1117                     // code could be rewritten to use FileChannel.
1118                     for (ByteBuffer outputChunk : outputChunks) {
1119                         outputFile.write(
1120                                 outputChunk.array(),
1121                                 outputChunk.arrayOffset() + outputChunk.position(),
1122                                 outputChunk.remaining());
1123                         outputChunk.position(outputChunk.limit());
1124                     }
1125 
1126                     outputFile.close();
1127                     outputFile = null;
1128                     apkSigner.outputDone();
1129                 }
1130 
1131                 return;
1132             }
1133         } catch (Exception e) {
1134             e.printStackTrace();
1135             System.exit(1);
1136         } finally {
1137             try {
1138                 if (inputJar != null) inputJar.close();
1139                 if (outputFile != null) outputFile.close();
1140             } catch (IOException e) {
1141                 e.printStackTrace();
1142                 System.exit(1);
1143             }
1144         }
1145     }
1146 }
1147