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