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.CMSProcessableByteArray;
     27 import org.bouncycastle.cms.CMSSignedData;
     28 import org.bouncycastle.cms.CMSSignedDataGenerator;
     29 import org.bouncycastle.cms.CMSTypedData;
     30 import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
     31 import org.bouncycastle.jce.provider.BouncyCastleProvider;
     32 import org.bouncycastle.operator.ContentSigner;
     33 import org.bouncycastle.operator.OperatorCreationException;
     34 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
     35 import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
     36 import org.bouncycastle.util.encoders.Base64;
     37 
     38 import java.io.Console;
     39 import java.io.BufferedReader;
     40 import java.io.ByteArrayInputStream;
     41 import java.io.ByteArrayOutputStream;
     42 import java.io.DataInputStream;
     43 import java.io.File;
     44 import java.io.FileInputStream;
     45 import java.io.FileOutputStream;
     46 import java.io.FilterOutputStream;
     47 import java.io.IOException;
     48 import java.io.InputStream;
     49 import java.io.InputStreamReader;
     50 import java.io.OutputStream;
     51 import java.io.PrintStream;
     52 import java.lang.reflect.Constructor;
     53 import java.security.DigestOutputStream;
     54 import java.security.GeneralSecurityException;
     55 import java.security.Key;
     56 import java.security.KeyFactory;
     57 import java.security.MessageDigest;
     58 import java.security.PrivateKey;
     59 import java.security.Provider;
     60 import java.security.Security;
     61 import java.security.cert.CertificateEncodingException;
     62 import java.security.cert.CertificateFactory;
     63 import java.security.cert.X509Certificate;
     64 import java.security.spec.InvalidKeySpecException;
     65 import java.security.spec.PKCS8EncodedKeySpec;
     66 import java.util.ArrayList;
     67 import java.util.Collections;
     68 import java.util.Enumeration;
     69 import java.util.Locale;
     70 import java.util.Map;
     71 import java.util.TreeMap;
     72 import java.util.jar.Attributes;
     73 import java.util.jar.JarEntry;
     74 import java.util.jar.JarFile;
     75 import java.util.jar.JarOutputStream;
     76 import java.util.jar.Manifest;
     77 import java.util.regex.Pattern;
     78 import javax.crypto.Cipher;
     79 import javax.crypto.EncryptedPrivateKeyInfo;
     80 import javax.crypto.SecretKeyFactory;
     81 import javax.crypto.spec.PBEKeySpec;
     82 
     83 /**
     84  * HISTORICAL NOTE:
     85  *
     86  * Prior to the keylimepie release, SignApk ignored the signature
     87  * algorithm specified in the certificate and always used SHA1withRSA.
     88  *
     89  * Starting with JB-MR2, the platform supports SHA256withRSA, so we use
     90  * the signature algorithm in the certificate to select which to use
     91  * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported.
     92  *
     93  * Because there are old keys still in use whose certificate actually
     94  * says "MD5withRSA", we treat these as though they say "SHA1withRSA"
     95  * for compatibility with older releases.  This can be changed by
     96  * altering the getAlgorithm() function below.
     97  */
     98 
     99 
    100 /**
    101  * Command line tool to sign JAR files (including APKs and OTA updates) in a way
    102  * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or
    103  * SHA-256 (see historical note).
    104  */
    105 class SignApk {
    106     private static final String CERT_SF_NAME = "META-INF/CERT.SF";
    107     private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
    108     private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
    109     private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s";
    110 
    111     private static final String OTACERT_NAME = "META-INF/com/android/otacert";
    112 
    113     private static Provider sBouncyCastleProvider;
    114 
    115     // bitmasks for which hash algorithms we need the manifest to include.
    116     private static final int USE_SHA1 = 1;
    117     private static final int USE_SHA256 = 2;
    118 
    119     /**
    120      * Return one of USE_SHA1 or USE_SHA256 according to the signature
    121      * algorithm specified in the cert.
    122      */
    123     private static int getDigestAlgorithm(X509Certificate cert) {
    124         String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
    125         if ("SHA1WITHRSA".equals(sigAlg) ||
    126             "MD5WITHRSA".equals(sigAlg)) {     // see "HISTORICAL NOTE" above.
    127             return USE_SHA1;
    128         } else if (sigAlg.startsWith("SHA256WITH")) {
    129             return USE_SHA256;
    130         } else {
    131             throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
    132                                                "\" in cert [" + cert.getSubjectDN());
    133         }
    134     }
    135 
    136     /** Returns the expected signature algorithm for this key type. */
    137     private static String getSignatureAlgorithm(X509Certificate cert) {
    138         String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
    139         String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
    140         if ("RSA".equalsIgnoreCase(keyType)) {
    141             if (getDigestAlgorithm(cert) == USE_SHA256) {
    142                 return "SHA256withRSA";
    143             } else {
    144                 return "SHA1withRSA";
    145             }
    146         } else if ("EC".equalsIgnoreCase(keyType)) {
    147             return "SHA256withECDSA";
    148         } else {
    149             throw new IllegalArgumentException("unsupported key type: " + keyType);
    150         }
    151     }
    152 
    153     // Files matching this pattern are not copied to the output.
    154     private static Pattern stripPattern =
    155         Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
    156                         Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
    157 
    158     private static X509Certificate readPublicKey(File file)
    159         throws IOException, GeneralSecurityException {
    160         FileInputStream input = new FileInputStream(file);
    161         try {
    162             CertificateFactory cf = CertificateFactory.getInstance("X.509");
    163             return (X509Certificate) cf.generateCertificate(input);
    164         } finally {
    165             input.close();
    166         }
    167     }
    168 
    169     /**
    170      * Reads the password from console and returns it as a string.
    171      *
    172      * @param keyFile The file containing the private key.  Used to prompt the user.
    173      */
    174     private static String readPassword(File keyFile) {
    175         Console console;
    176         char[] pwd;
    177         if((console = System.console()) != null &&
    178            (pwd = console.readPassword("[%s]", "Enter password for " + keyFile)) != null){
    179             return String.valueOf(pwd);
    180         } else {
    181             return null;
    182         }
    183     }
    184 
    185     /**
    186      * Decrypt an encrypted PKCS#8 format private key.
    187      *
    188      * Based on ghstark's post on Aug 6, 2006 at
    189      * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
    190      *
    191      * @param encryptedPrivateKey The raw data of the private key
    192      * @param keyFile The file containing the private key
    193      */
    194     private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
    195         throws GeneralSecurityException {
    196         EncryptedPrivateKeyInfo epkInfo;
    197         try {
    198             epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
    199         } catch (IOException ex) {
    200             // Probably not an encrypted key.
    201             return null;
    202         }
    203 
    204         char[] password = readPassword(keyFile).toCharArray();
    205 
    206         SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
    207         Key key = skFactory.generateSecret(new PBEKeySpec(password));
    208 
    209         Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
    210         cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
    211 
    212         try {
    213             return epkInfo.getKeySpec(cipher);
    214         } catch (InvalidKeySpecException ex) {
    215             System.err.println("signapk: Password for " + keyFile + " may be bad.");
    216             throw ex;
    217         }
    218     }
    219 
    220     /** Read a PKCS#8 format private key. */
    221     private static PrivateKey readPrivateKey(File file)
    222         throws IOException, GeneralSecurityException {
    223         DataInputStream input = new DataInputStream(new FileInputStream(file));
    224         try {
    225             byte[] bytes = new byte[(int) file.length()];
    226             input.read(bytes);
    227 
    228             /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
    229             PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file);
    230             if (spec == null) {
    231                 spec = new PKCS8EncodedKeySpec(bytes);
    232             }
    233 
    234             /*
    235              * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
    236              * OID and use that to construct a KeyFactory.
    237              */
    238             ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()));
    239             PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject());
    240             String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
    241 
    242             return KeyFactory.getInstance(algOid).generatePrivate(spec);
    243         } finally {
    244             input.close();
    245         }
    246     }
    247 
    248     /**
    249      * Add the hash(es) of every file to the manifest, creating it if
    250      * necessary.
    251      */
    252     private static Manifest addDigestsToManifest(JarFile jar, int hashes)
    253         throws IOException, GeneralSecurityException {
    254         Manifest input = jar.getManifest();
    255         Manifest output = new Manifest();
    256         Attributes main = output.getMainAttributes();
    257         if (input != null) {
    258             main.putAll(input.getMainAttributes());
    259         } else {
    260             main.putValue("Manifest-Version", "1.0");
    261             main.putValue("Created-By", "1.0 (Android SignApk)");
    262         }
    263 
    264         MessageDigest md_sha1 = null;
    265         MessageDigest md_sha256 = null;
    266         if ((hashes & USE_SHA1) != 0) {
    267             md_sha1 = MessageDigest.getInstance("SHA1");
    268         }
    269         if ((hashes & USE_SHA256) != 0) {
    270             md_sha256 = MessageDigest.getInstance("SHA256");
    271         }
    272 
    273         byte[] buffer = new byte[4096];
    274         int num;
    275 
    276         // We sort the input entries by name, and add them to the
    277         // output manifest in sorted order.  We expect that the output
    278         // map will be deterministic.
    279 
    280         TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
    281 
    282         for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
    283             JarEntry entry = e.nextElement();
    284             byName.put(entry.getName(), entry);
    285         }
    286 
    287         for (JarEntry entry: byName.values()) {
    288             String name = entry.getName();
    289             if (!entry.isDirectory() &&
    290                 (stripPattern == null || !stripPattern.matcher(name).matches())) {
    291                 InputStream data = jar.getInputStream(entry);
    292                 while ((num = data.read(buffer)) > 0) {
    293                     if (md_sha1 != null) md_sha1.update(buffer, 0, num);
    294                     if (md_sha256 != null) md_sha256.update(buffer, 0, num);
    295                 }
    296 
    297                 Attributes attr = null;
    298                 if (input != null) attr = input.getAttributes(name);
    299                 attr = attr != null ? new Attributes(attr) : new Attributes();
    300                 if (md_sha1 != null) {
    301                     attr.putValue("SHA1-Digest",
    302                                   new String(Base64.encode(md_sha1.digest()), "ASCII"));
    303                 }
    304                 if (md_sha256 != null) {
    305                     attr.putValue("SHA-256-Digest",
    306                                   new String(Base64.encode(md_sha256.digest()), "ASCII"));
    307                 }
    308                 output.getEntries().put(name, attr);
    309             }
    310         }
    311 
    312         return output;
    313     }
    314 
    315     /**
    316      * Add a copy of the public key to the archive; this should
    317      * exactly match one of the files in
    318      * /system/etc/security/otacerts.zip on the device.  (The same
    319      * cert can be extracted from the CERT.RSA file but this is much
    320      * easier to get at.)
    321      */
    322     private static void addOtacert(JarOutputStream outputJar,
    323                                    File publicKeyFile,
    324                                    long timestamp,
    325                                    Manifest manifest,
    326                                    int hash)
    327         throws IOException, GeneralSecurityException {
    328         MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256");
    329 
    330         JarEntry je = new JarEntry(OTACERT_NAME);
    331         je.setTime(timestamp);
    332         outputJar.putNextEntry(je);
    333         FileInputStream input = new FileInputStream(publicKeyFile);
    334         byte[] b = new byte[4096];
    335         int read;
    336         while ((read = input.read(b)) != -1) {
    337             outputJar.write(b, 0, read);
    338             md.update(b, 0, read);
    339         }
    340         input.close();
    341 
    342         Attributes attr = new Attributes();
    343         attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest",
    344                       new String(Base64.encode(md.digest()), "ASCII"));
    345         manifest.getEntries().put(OTACERT_NAME, attr);
    346     }
    347 
    348 
    349     /** Write to another stream and track how many bytes have been
    350      *  written.
    351      */
    352     private static class CountOutputStream extends FilterOutputStream {
    353         private int mCount;
    354 
    355         public CountOutputStream(OutputStream out) {
    356             super(out);
    357             mCount = 0;
    358         }
    359 
    360         @Override
    361         public void write(int b) throws IOException {
    362             super.write(b);
    363             mCount++;
    364         }
    365 
    366         @Override
    367         public void write(byte[] b, int off, int len) throws IOException {
    368             super.write(b, off, len);
    369             mCount += len;
    370         }
    371 
    372         public int size() {
    373             return mCount;
    374         }
    375     }
    376 
    377     /** Write a .SF file with a digest of the specified manifest. */
    378     private static void writeSignatureFile(Manifest manifest, OutputStream out,
    379                                            int hash)
    380         throws IOException, GeneralSecurityException {
    381         Manifest sf = new Manifest();
    382         Attributes main = sf.getMainAttributes();
    383         main.putValue("Signature-Version", "1.0");
    384         main.putValue("Created-By", "1.0 (Android SignApk)");
    385 
    386         MessageDigest md = MessageDigest.getInstance(
    387             hash == USE_SHA256 ? "SHA256" : "SHA1");
    388         PrintStream print = new PrintStream(
    389             new DigestOutputStream(new ByteArrayOutputStream(), md),
    390             true, "UTF-8");
    391 
    392         // Digest of the entire manifest
    393         manifest.write(print);
    394         print.flush();
    395         main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
    396                       new String(Base64.encode(md.digest()), "ASCII"));
    397 
    398         Map<String, Attributes> entries = manifest.getEntries();
    399         for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
    400             // Digest of the manifest stanza for this entry.
    401             print.print("Name: " + entry.getKey() + "\r\n");
    402             for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
    403                 print.print(att.getKey() + ": " + att.getValue() + "\r\n");
    404             }
    405             print.print("\r\n");
    406             print.flush();
    407 
    408             Attributes sfAttr = new Attributes();
    409             sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest",
    410                             new String(Base64.encode(md.digest()), "ASCII"));
    411             sf.getEntries().put(entry.getKey(), sfAttr);
    412         }
    413 
    414         CountOutputStream cout = new CountOutputStream(out);
    415         sf.write(cout);
    416 
    417         // A bug in the java.util.jar implementation of Android platforms
    418         // up to version 1.6 will cause a spurious IOException to be thrown
    419         // if the length of the signature file is a multiple of 1024 bytes.
    420         // As a workaround, add an extra CRLF in this case.
    421         if ((cout.size() % 1024) == 0) {
    422             cout.write('\r');
    423             cout.write('\n');
    424         }
    425     }
    426 
    427     /** Sign data and write the digital signature to 'out'. */
    428     private static void writeSignatureBlock(
    429         CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
    430         OutputStream out)
    431         throws IOException,
    432                CertificateEncodingException,
    433                OperatorCreationException,
    434                CMSException {
    435         ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
    436         certList.add(publicKey);
    437         JcaCertStore certs = new JcaCertStore(certList);
    438 
    439         CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
    440         ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
    441             .setProvider(sBouncyCastleProvider)
    442             .build(privateKey);
    443         gen.addSignerInfoGenerator(
    444             new JcaSignerInfoGeneratorBuilder(
    445                 new JcaDigestCalculatorProviderBuilder()
    446                 .setProvider(sBouncyCastleProvider)
    447                 .build())
    448             .setDirectSignature(true)
    449             .build(signer, publicKey));
    450         gen.addCertificates(certs);
    451         CMSSignedData sigData = gen.generate(data, false);
    452 
    453         ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
    454         DEROutputStream dos = new DEROutputStream(out);
    455         dos.writeObject(asn1.readObject());
    456     }
    457 
    458     /**
    459      * Copy all the files in a manifest from input to output.  We set
    460      * the modification times in the output to a fixed time, so as to
    461      * reduce variation in the output file and make incremental OTAs
    462      * more efficient.
    463      */
    464     private static void copyFiles(Manifest manifest, JarFile in, JarOutputStream out,
    465                                   long timestamp, int alignment) throws IOException {
    466         byte[] buffer = new byte[4096];
    467         int num;
    468 
    469         Map<String, Attributes> entries = manifest.getEntries();
    470         ArrayList<String> names = new ArrayList<String>(entries.keySet());
    471         Collections.sort(names);
    472 
    473         boolean firstEntry = true;
    474         long offset = 0L;
    475 
    476         // We do the copy in two passes -- first copying all the
    477         // entries that are STORED, then copying all the entries that
    478         // have any other compression flag (which in practice means
    479         // DEFLATED).  This groups all the stored entries together at
    480         // the start of the file and makes it easier to do alignment
    481         // on them (since only stored entries are aligned).
    482 
    483         for (String name : names) {
    484             JarEntry inEntry = in.getJarEntry(name);
    485             JarEntry outEntry = null;
    486             if (inEntry.getMethod() != JarEntry.STORED) continue;
    487             // Preserve the STORED method of the input entry.
    488             outEntry = new JarEntry(inEntry);
    489             outEntry.setTime(timestamp);
    490 
    491             // 'offset' is the offset into the file at which we expect
    492             // the file data to begin.  This is the value we need to
    493             // make a multiple of 'alignement'.
    494             offset += JarFile.LOCHDR + outEntry.getName().length();
    495             if (firstEntry) {
    496                 // The first entry in a jar file has an extra field of
    497                 // four bytes that you can't get rid of; any extra
    498                 // data you specify in the JarEntry is appended to
    499                 // these forced four bytes.  This is JAR_MAGIC in
    500                 // JarOutputStream; the bytes are 0xfeca0000.
    501                 offset += 4;
    502                 firstEntry = false;
    503             }
    504             if (alignment > 0 && (offset % alignment != 0)) {
    505                 // Set the "extra data" of the entry to between 1 and
    506                 // alignment-1 bytes, to make the file data begin at
    507                 // an aligned offset.
    508                 int needed = alignment - (int)(offset % alignment);
    509                 outEntry.setExtra(new byte[needed]);
    510                 offset += needed;
    511             }
    512 
    513             out.putNextEntry(outEntry);
    514 
    515             InputStream data = in.getInputStream(inEntry);
    516             while ((num = data.read(buffer)) > 0) {
    517                 out.write(buffer, 0, num);
    518                 offset += num;
    519             }
    520             out.flush();
    521         }
    522 
    523         // Copy all the non-STORED entries.  We don't attempt to
    524         // maintain the 'offset' variable past this point; we don't do
    525         // alignment on these entries.
    526 
    527         for (String name : names) {
    528             JarEntry inEntry = in.getJarEntry(name);
    529             JarEntry outEntry = null;
    530             if (inEntry.getMethod() == JarEntry.STORED) continue;
    531             // Create a new entry so that the compressed len is recomputed.
    532             outEntry = new JarEntry(name);
    533             outEntry.setTime(timestamp);
    534             out.putNextEntry(outEntry);
    535 
    536             InputStream data = in.getInputStream(inEntry);
    537             while ((num = data.read(buffer)) > 0) {
    538                 out.write(buffer, 0, num);
    539             }
    540             out.flush();
    541         }
    542     }
    543 
    544     private static class WholeFileSignerOutputStream extends FilterOutputStream {
    545         private boolean closing = false;
    546         private ByteArrayOutputStream footer = new ByteArrayOutputStream();
    547         private OutputStream tee;
    548 
    549         public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
    550             super(out);
    551             this.tee = tee;
    552         }
    553 
    554         public void notifyClosing() {
    555             closing = true;
    556         }
    557 
    558         public void finish() throws IOException {
    559             closing = false;
    560 
    561             byte[] data = footer.toByteArray();
    562             if (data.length < 2)
    563                 throw new IOException("Less than two bytes written to footer");
    564             write(data, 0, data.length - 2);
    565         }
    566 
    567         public byte[] getTail() {
    568             return footer.toByteArray();
    569         }
    570 
    571         @Override
    572         public void write(byte[] b) throws IOException {
    573             write(b, 0, b.length);
    574         }
    575 
    576         @Override
    577         public void write(byte[] b, int off, int len) throws IOException {
    578             if (closing) {
    579                 // if the jar is about to close, save the footer that will be written
    580                 footer.write(b, off, len);
    581             }
    582             else {
    583                 // write to both output streams. out is the CMSTypedData signer and tee is the file.
    584                 out.write(b, off, len);
    585                 tee.write(b, off, len);
    586             }
    587         }
    588 
    589         @Override
    590         public void write(int b) throws IOException {
    591             if (closing) {
    592                 // if the jar is about to close, save the footer that will be written
    593                 footer.write(b);
    594             }
    595             else {
    596                 // write to both output streams. out is the CMSTypedData signer and tee is the file.
    597                 out.write(b);
    598                 tee.write(b);
    599             }
    600         }
    601     }
    602 
    603     private static class CMSSigner implements CMSTypedData {
    604         private JarFile inputJar;
    605         private File publicKeyFile;
    606         private X509Certificate publicKey;
    607         private PrivateKey privateKey;
    608         private String outputFile;
    609         private OutputStream outputStream;
    610         private final ASN1ObjectIdentifier type;
    611         private WholeFileSignerOutputStream signer;
    612 
    613         public CMSSigner(JarFile inputJar, File publicKeyFile,
    614                          X509Certificate publicKey, PrivateKey privateKey,
    615                          OutputStream outputStream) {
    616             this.inputJar = inputJar;
    617             this.publicKeyFile = publicKeyFile;
    618             this.publicKey = publicKey;
    619             this.privateKey = privateKey;
    620             this.outputStream = outputStream;
    621             this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
    622         }
    623 
    624         /**
    625          * This should actually return byte[] or something similar, but nothing
    626          * actually checks it currently.
    627          */
    628         public Object getContent() {
    629             return this;
    630         }
    631 
    632         public ASN1ObjectIdentifier getContentType() {
    633             return type;
    634         }
    635 
    636         public void write(OutputStream out) throws IOException {
    637             try {
    638                 signer = new WholeFileSignerOutputStream(out, outputStream);
    639                 JarOutputStream outputJar = new JarOutputStream(signer);
    640 
    641                 int hash = getDigestAlgorithm(publicKey);
    642 
    643                 // Assume the certificate is valid for at least an hour.
    644                 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
    645 
    646                 Manifest manifest = addDigestsToManifest(inputJar, hash);
    647                 copyFiles(manifest, inputJar, outputJar, timestamp, 0);
    648                 addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
    649 
    650                 signFile(manifest, inputJar,
    651                          new X509Certificate[]{ publicKey },
    652                          new PrivateKey[]{ privateKey },
    653                          outputJar);
    654 
    655                 signer.notifyClosing();
    656                 outputJar.close();
    657                 signer.finish();
    658             }
    659             catch (Exception e) {
    660                 throw new IOException(e);
    661             }
    662         }
    663 
    664         public void writeSignatureBlock(ByteArrayOutputStream temp)
    665             throws IOException,
    666                    CertificateEncodingException,
    667                    OperatorCreationException,
    668                    CMSException {
    669             SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
    670         }
    671 
    672         public WholeFileSignerOutputStream getSigner() {
    673             return signer;
    674         }
    675     }
    676 
    677     private static void signWholeFile(JarFile inputJar, File publicKeyFile,
    678                                       X509Certificate publicKey, PrivateKey privateKey,
    679                                       OutputStream outputStream) throws Exception {
    680         CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
    681                                          publicKey, privateKey, outputStream);
    682 
    683         ByteArrayOutputStream temp = new ByteArrayOutputStream();
    684 
    685         // put a readable message and a null char at the start of the
    686         // archive comment, so that tools that display the comment
    687         // (hopefully) show something sensible.
    688         // TODO: anything more useful we can put in this message?
    689         byte[] message = "signed by SignApk".getBytes("UTF-8");
    690         temp.write(message);
    691         temp.write(0);
    692 
    693         cmsOut.writeSignatureBlock(temp);
    694 
    695         byte[] zipData = cmsOut.getSigner().getTail();
    696 
    697         // For a zip with no archive comment, the
    698         // end-of-central-directory record will be 22 bytes long, so
    699         // we expect to find the EOCD marker 22 bytes from the end.
    700         if (zipData[zipData.length-22] != 0x50 ||
    701             zipData[zipData.length-21] != 0x4b ||
    702             zipData[zipData.length-20] != 0x05 ||
    703             zipData[zipData.length-19] != 0x06) {
    704             throw new IllegalArgumentException("zip data already has an archive comment");
    705         }
    706 
    707         int total_size = temp.size() + 6;
    708         if (total_size > 0xffff) {
    709             throw new IllegalArgumentException("signature is too big for ZIP file comment");
    710         }
    711         // signature starts this many bytes from the end of the file
    712         int signature_start = total_size - message.length - 1;
    713         temp.write(signature_start & 0xff);
    714         temp.write((signature_start >> 8) & 0xff);
    715         // Why the 0xff bytes?  In a zip file with no archive comment,
    716         // bytes [-6:-2] of the file are the little-endian offset from
    717         // the start of the file to the central directory.  So for the
    718         // two high bytes to be 0xff 0xff, the archive would have to
    719         // be nearly 4GB in size.  So it's unlikely that a real
    720         // commentless archive would have 0xffs here, and lets us tell
    721         // an old signed archive from a new one.
    722         temp.write(0xff);
    723         temp.write(0xff);
    724         temp.write(total_size & 0xff);
    725         temp.write((total_size >> 8) & 0xff);
    726         temp.flush();
    727 
    728         // Signature verification checks that the EOCD header is the
    729         // last such sequence in the file (to avoid minzip finding a
    730         // fake EOCD appended after the signature in its scan).  The
    731         // odds of producing this sequence by chance are very low, but
    732         // let's catch it here if it does.
    733         byte[] b = temp.toByteArray();
    734         for (int i = 0; i < b.length-3; ++i) {
    735             if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
    736                 throw new IllegalArgumentException("found spurious EOCD header at " + i);
    737             }
    738         }
    739 
    740         outputStream.write(total_size & 0xff);
    741         outputStream.write((total_size >> 8) & 0xff);
    742         temp.writeTo(outputStream);
    743     }
    744 
    745     private static void signFile(Manifest manifest, JarFile inputJar,
    746                                  X509Certificate[] publicKey, PrivateKey[] privateKey,
    747                                  JarOutputStream outputJar)
    748         throws Exception {
    749         // Assume the certificate is valid for at least an hour.
    750         long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
    751 
    752         // MANIFEST.MF
    753         JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
    754         je.setTime(timestamp);
    755         outputJar.putNextEntry(je);
    756         manifest.write(outputJar);
    757 
    758         int numKeys = publicKey.length;
    759         for (int k = 0; k < numKeys; ++k) {
    760             // CERT.SF / CERT#.SF
    761             je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
    762                               (String.format(CERT_SF_MULTI_NAME, k)));
    763             je.setTime(timestamp);
    764             outputJar.putNextEntry(je);
    765             ByteArrayOutputStream baos = new ByteArrayOutputStream();
    766             writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
    767             byte[] signedData = baos.toByteArray();
    768             outputJar.write(signedData);
    769 
    770             // CERT.{EC,RSA} / CERT#.{EC,RSA}
    771             final String keyType = publicKey[k].getPublicKey().getAlgorithm();
    772             je = new JarEntry(numKeys == 1 ?
    773                               (String.format(CERT_SIG_NAME, keyType)) :
    774                               (String.format(CERT_SIG_MULTI_NAME, k, keyType)));
    775             je.setTime(timestamp);
    776             outputJar.putNextEntry(je);
    777             writeSignatureBlock(new CMSProcessableByteArray(signedData),
    778                                 publicKey[k], privateKey[k], outputJar);
    779         }
    780     }
    781 
    782     /**
    783      * Tries to load a JSE Provider by class name. This is for custom PrivateKey
    784      * types that might be stored in PKCS#11-like storage.
    785      */
    786     private static void loadProviderIfNecessary(String providerClassName) {
    787         if (providerClassName == null) {
    788             return;
    789         }
    790 
    791         final Class<?> klass;
    792         try {
    793             final ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
    794             if (sysLoader != null) {
    795                 klass = sysLoader.loadClass(providerClassName);
    796             } else {
    797                 klass = Class.forName(providerClassName);
    798             }
    799         } catch (ClassNotFoundException e) {
    800             e.printStackTrace();
    801             System.exit(1);
    802             return;
    803         }
    804 
    805         Constructor<?> constructor = null;
    806         for (Constructor<?> c : klass.getConstructors()) {
    807             if (c.getParameterTypes().length == 0) {
    808                 constructor = c;
    809                 break;
    810             }
    811         }
    812         if (constructor == null) {
    813             System.err.println("No zero-arg constructor found for " + providerClassName);
    814             System.exit(1);
    815             return;
    816         }
    817 
    818         final Object o;
    819         try {
    820             o = constructor.newInstance();
    821         } catch (Exception e) {
    822             e.printStackTrace();
    823             System.exit(1);
    824             return;
    825         }
    826         if (!(o instanceof Provider)) {
    827             System.err.println("Not a Provider class: " + providerClassName);
    828             System.exit(1);
    829         }
    830 
    831         Security.insertProviderAt((Provider) o, 1);
    832     }
    833 
    834     private static void usage() {
    835         System.err.println("Usage: signapk [-w] " +
    836                            "[-a <alignment>] " +
    837                            "[-providerClass <className>] " +
    838                            "publickey.x509[.pem] privatekey.pk8 " +
    839                            "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
    840                            "input.jar output.jar");
    841         System.exit(2);
    842     }
    843 
    844     public static void main(String[] args) {
    845         if (args.length < 4) usage();
    846 
    847         sBouncyCastleProvider = new BouncyCastleProvider();
    848         Security.addProvider(sBouncyCastleProvider);
    849 
    850         boolean signWholeFile = false;
    851         String providerClass = null;
    852         String providerArg = null;
    853         int alignment = 4;
    854 
    855         int argstart = 0;
    856         while (argstart < args.length && args[argstart].startsWith("-")) {
    857             if ("-w".equals(args[argstart])) {
    858                 signWholeFile = true;
    859                 ++argstart;
    860             } else if ("-providerClass".equals(args[argstart])) {
    861                 if (argstart + 1 >= args.length) {
    862                     usage();
    863                 }
    864                 providerClass = args[++argstart];
    865                 ++argstart;
    866             } else if ("-a".equals(args[argstart])) {
    867                 alignment = Integer.parseInt(args[++argstart]);
    868                 ++argstart;
    869             } else {
    870                 usage();
    871             }
    872         }
    873 
    874         if ((args.length - argstart) % 2 == 1) usage();
    875         int numKeys = ((args.length - argstart) / 2) - 1;
    876         if (signWholeFile && numKeys > 1) {
    877             System.err.println("Only one key may be used with -w.");
    878             System.exit(2);
    879         }
    880 
    881         loadProviderIfNecessary(providerClass);
    882 
    883         String inputFilename = args[args.length-2];
    884         String outputFilename = args[args.length-1];
    885 
    886         JarFile inputJar = null;
    887         FileOutputStream outputFile = null;
    888         int hashes = 0;
    889 
    890         try {
    891             File firstPublicKeyFile = new File(args[argstart+0]);
    892 
    893             X509Certificate[] publicKey = new X509Certificate[numKeys];
    894             try {
    895                 for (int i = 0; i < numKeys; ++i) {
    896                     int argNum = argstart + i*2;
    897                     publicKey[i] = readPublicKey(new File(args[argNum]));
    898                     hashes |= getDigestAlgorithm(publicKey[i]);
    899                 }
    900             } catch (IllegalArgumentException e) {
    901                 System.err.println(e);
    902                 System.exit(1);
    903             }
    904 
    905             // Set the ZIP file timestamp to the starting valid time
    906             // of the 0th certificate plus one hour (to match what
    907             // we've historically done).
    908             long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
    909 
    910             PrivateKey[] privateKey = new PrivateKey[numKeys];
    911             for (int i = 0; i < numKeys; ++i) {
    912                 int argNum = argstart + i*2 + 1;
    913                 privateKey[i] = readPrivateKey(new File(args[argNum]));
    914             }
    915             inputJar = new JarFile(new File(inputFilename), false);  // Don't verify.
    916 
    917             outputFile = new FileOutputStream(outputFilename);
    918 
    919 
    920             if (signWholeFile) {
    921                 SignApk.signWholeFile(inputJar, firstPublicKeyFile,
    922                                       publicKey[0], privateKey[0], outputFile);
    923             } else {
    924                 JarOutputStream outputJar = new JarOutputStream(outputFile);
    925 
    926                 // For signing .apks, use the maximum compression to make
    927                 // them as small as possible (since they live forever on
    928                 // the system partition).  For OTA packages, use the
    929                 // default compression level, which is much much faster
    930                 // and produces output that is only a tiny bit larger
    931                 // (~0.1% on full OTA packages I tested).
    932                 outputJar.setLevel(9);
    933 
    934                 Manifest manifest = addDigestsToManifest(inputJar, hashes);
    935                 copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
    936                 signFile(manifest, inputJar, publicKey, privateKey, outputJar);
    937                 outputJar.close();
    938             }
    939         } catch (Exception e) {
    940             e.printStackTrace();
    941             System.exit(1);
    942         } finally {
    943             try {
    944                 if (inputJar != null) inputJar.close();
    945                 if (outputFile != null) outputFile.close();
    946             } catch (IOException e) {
    947                 e.printStackTrace();
    948                 System.exit(1);
    949             }
    950         }
    951     }
    952 }
    953