Home | History | Annotate | Download | only in signapk
      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      */
    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      */
    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 
    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      */
    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      */
    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. */
    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      */
    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'. */
    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      */
    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      */
    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 
    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 
    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      */
    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 
    581         public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
    582             super(out);
    583             this.tee = tee;
    584         }
    585 
    586         public void notifyClosing() {
    587             closing = true;
    588         }
    589 
    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 
    599         public byte[] getTail() {
    600             return footer.toByteArray();
    601         }
    602 
    603         @Override
    604         public void write(byte[] b) throws IOException {
    605             write(b, 0, b.length);
    606         }
    607 
    608         @Override
    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
    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 
    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
    669         public Object getContent() {
    670             return this;
    671         }
    672 
    673         @Override
    674         public ASN1ObjectIdentifier getContentType() {
    675             return type;
    676         }
    677 
    678         @Override
    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 
    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 
    704         public WholeFileSignerOutputStream getSigner() {
    705             return signer;
    706         }
    707     }
    708 
    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      */
    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 
    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 
    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      */
    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 
    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 
    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 
    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